diff options
Diffstat (limited to 'java/src/com/android/inputmethod')
46 files changed, 1443 insertions, 518 deletions
diff --git a/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java b/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java new file mode 100644 index 000000000..ff6561c58 --- /dev/null +++ b/java/src/com/android/inputmethod/compat/UserDictionaryCompatUtils.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2013 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.compat; + +import android.content.Context; +import android.provider.UserDictionary.Words; + +import java.lang.reflect.Method; +import java.util.Locale; + +public final class UserDictionaryCompatUtils { + // UserDictionary.Words#addWord(Context, String, int, String, Locale) was introduced + // in API level 16 (Build.VERSION_CODES.JELLY_BEAN). + private static final Method METHOD_addWord = CompatUtils.getMethod(Words.class, "addWord", + Context.class, String.class, Integer.TYPE, String.class, Locale.class); + + public static void addWord(final Context context, final String word, final int freq, + final String shortcut, final Locale locale) { + if (hasNewerAddWord()) { + CompatUtils.invoke(Words.class, null, METHOD_addWord, context, word, freq, shortcut, + locale); + } else { + // Fall back to the pre-JellyBean method. + final int localeType; + if (null == locale) { + localeType = Words.LOCALE_TYPE_ALL; + } else { + localeType = Words.LOCALE_TYPE_CURRENT; + } + Words.addWord(context, word, freq, localeType); + } + } + + private static final boolean hasNewerAddWord() { + return null != METHOD_addWord; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index d15f14f88..4e41b77ce 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -207,7 +207,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { * Update keyboard shift state triggered by connected EditText status change. */ public void updateShiftState() { - mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState()); + mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState(), + mLatinIME.getCurrentRecapitalizeState()); } // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout @@ -276,7 +277,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { // Implements {@link KeyboardState.SwitchActions}. @Override public void requestUpdatingShiftState() { - mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState()); + mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState(), + mLatinIME.getCurrentRecapitalizeState()); } // Implements {@link KeyboardState.SwitchActions}. diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java index 43d28be5d..e4e75c342 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java @@ -26,10 +26,8 @@ import android.graphics.Paint.Align; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.Region; -import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.util.AttributeSet; -import android.util.SparseArray; import android.view.View; import com.android.inputmethod.keyboard.internal.KeyDrawParams; @@ -73,15 +71,15 @@ import java.util.HashSet; */ public class KeyboardView extends View { // XML attributes - protected final KeyVisualAttributes mKeyVisualAttributes; + private final KeyVisualAttributes mKeyVisualAttributes; private final int mKeyLabelHorizontalPadding; private final float mKeyHintLetterPadding; private final float mKeyPopupHintLetterPadding; private final float mKeyShiftedLetterHintPadding; private final float mKeyTextShadowRadius; - protected final float mVerticalCorrection; - protected final Drawable mKeyBackground; - protected final Rect mKeyBackgroundPadding = new Rect(); + private final float mVerticalCorrection; + private final Drawable mKeyBackground; + private final Rect mKeyBackgroundPadding = new Rect(); // HORIZONTAL ELLIPSIS "...", character for popup hint. private static final String POPUP_HINT_CHAR = "\u2026"; @@ -113,10 +111,6 @@ public class KeyboardView extends View { private final Canvas mOffscreenCanvas = new Canvas(); private final Paint mPaint = new Paint(); private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); - // This sparse array caches key label text height in pixel indexed by key label text size. - private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray(); - // This sparse array caches key label text width in pixel indexed by key label text size. - private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray(); private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' }; private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' }; @@ -134,15 +128,15 @@ public class KeyboardView extends View { mKeyLabelHorizontalPadding = keyboardViewAttr.getDimensionPixelOffset( R.styleable.KeyboardView_keyLabelHorizontalPadding, 0); mKeyHintLetterPadding = keyboardViewAttr.getDimension( - R.styleable.KeyboardView_keyHintLetterPadding, 0); + R.styleable.KeyboardView_keyHintLetterPadding, 0.0f); mKeyPopupHintLetterPadding = keyboardViewAttr.getDimension( - R.styleable.KeyboardView_keyPopupHintLetterPadding, 0); + R.styleable.KeyboardView_keyPopupHintLetterPadding, 0.0f); mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension( - R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0); + R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0.0f); mKeyTextShadowRadius = keyboardViewAttr.getFloat( R.styleable.KeyboardView_keyTextShadowRadius, 0.0f); mVerticalCorrection = keyboardViewAttr.getDimension( - R.styleable.KeyboardView_verticalCorrection, 0); + R.styleable.KeyboardView_verticalCorrection, 0.0f); keyboardViewAttr.recycle(); final TypedArray keyAttr = context.obtainStyledAttributes(attrs, @@ -185,6 +179,14 @@ public class KeyboardView extends View { return mKeyboard; } + protected float getVerticalCorrection() { + return mVerticalCorrection; + } + + protected void updateKeyDrawParams(final int keyHeight) { + mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes); + } + @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { if (mKeyboard != null) { @@ -213,7 +215,7 @@ public class KeyboardView extends View { } onDrawKeyboard(mOffscreenCanvas); } - canvas.drawBitmap(mOffscreenBuffer, 0, 0, null); + canvas.drawBitmap(mOffscreenBuffer, 0.0f, 0.0f, null); } private boolean maybeAllocateOffscreenBuffer() { @@ -333,7 +335,7 @@ public class KeyboardView extends View { canvas.translate(bgX, bgY); background.draw(canvas); if (LatinImeLogger.sVISUALDEBUG) { - drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint()); + drawRectangle(canvas, 0.0f, 0.0f, bgWidth, bgHeight, 0x80c00000, new Paint()); } canvas.translate(-bgX, -bgY); } @@ -347,7 +349,7 @@ public class KeyboardView extends View { final float centerY = keyHeight * 0.5f; if (LatinImeLogger.sVISUALDEBUG) { - drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint()); + drawRectangle(canvas, 0.0f, 0.0f, keyWidth, keyHeight, 0x800000c0, new Paint()); } // Draw key label. @@ -357,14 +359,16 @@ public class KeyboardView extends View { final String label = key.mLabel; paint.setTypeface(key.selectTypeface(params)); paint.setTextSize(key.selectTextSize(params)); - final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint); - final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint); + final float labelCharHeight = TypefaceUtils.getCharHeight( + KEY_LABEL_REFERENCE_CHAR, paint); + final float labelCharWidth = TypefaceUtils.getCharWidth( + KEY_LABEL_REFERENCE_CHAR, paint); // Vertical label text alignment. - final float baseline = centerY + labelCharHeight / 2; + final float baseline = centerY + labelCharHeight / 2.0f; // Horizontal label text alignment - float labelWidth = 0; + float labelWidth = 0.0f; if (key.isAlignLeft()) { positionX = mKeyLabelHorizontalPadding; paint.setTextAlign(Align.LEFT); @@ -373,31 +377,31 @@ public class KeyboardView extends View { paint.setTextAlign(Align.RIGHT); } else if (key.isAlignLeftOfCenter()) { // TODO: Parameterise this? - positionX = centerX - labelCharWidth * 7 / 4; + positionX = centerX - labelCharWidth * 7.0f / 4.0f; paint.setTextAlign(Align.LEFT); } else if (key.hasLabelWithIconLeft() && icon != null) { - labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() + labelWidth = TypefaceUtils.getLabelWidth(label, paint) + icon.getIntrinsicWidth() + LABEL_ICON_MARGIN * keyWidth; - positionX = centerX + labelWidth / 2; + positionX = centerX + labelWidth / 2.0f; paint.setTextAlign(Align.RIGHT); } else if (key.hasLabelWithIconRight() && icon != null) { - labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() + labelWidth = TypefaceUtils.getLabelWidth(label, paint) + icon.getIntrinsicWidth() + LABEL_ICON_MARGIN * keyWidth; - positionX = centerX - labelWidth / 2; + positionX = centerX - labelWidth / 2.0f; paint.setTextAlign(Align.LEFT); } else { positionX = centerX; paint.setTextAlign(Align.CENTER); } if (key.needsXScale()) { - paint.setTextScaleX( - Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint))); + paint.setTextScaleX(Math.min(1.0f, + (keyWidth * MAX_LABEL_RATIO) / TypefaceUtils.getLabelWidth(label, paint))); } paint.setColor(key.selectTextColor(params)); if (key.isEnabled()) { // Set a drop shadow for the text - paint.setShadowLayer(mKeyTextShadowRadius, 0, 0, params.mTextShadowColor); + paint.setShadowLayer(mKeyTextShadowRadius, 0.0f, 0.0f, params.mTextShadowColor); } else { // Make label invisible paint.setColor(Color.TRANSPARENT); @@ -405,7 +409,7 @@ public class KeyboardView extends View { blendAlpha(paint, params.mAnimAlpha); canvas.drawText(label, 0, label.length(), positionX, baseline, paint); // Turn off drop shadow and reset x-scale. - paint.setShadowLayer(0, 0, 0, 0); + paint.setShadowLayer(0.0f, 0.0f, 0.0f, Color.TRANSPARENT); paint.setTextScaleX(1.0f); if (icon != null) { @@ -413,10 +417,10 @@ public class KeyboardView extends View { final int iconHeight = icon.getIntrinsicHeight(); final int iconY = (keyHeight - iconHeight) / 2; if (key.hasLabelWithIconLeft()) { - final int iconX = (int)(centerX - labelWidth / 2); + final int iconX = (int)(centerX - labelWidth / 2.0f); drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); } else if (key.hasLabelWithIconRight()) { - final int iconX = (int)(centerX + labelWidth / 2 - iconWidth); + final int iconX = (int)(centerX + labelWidth / 2.0f - iconWidth); drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); } } @@ -439,20 +443,23 @@ public class KeyboardView extends View { // The hint label is placed just right of the key label. Used mainly on // "phone number" layout. // TODO: Generalize the following calculations. - hintX = positionX + getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2; - hintY = centerY + getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2; + hintX = positionX + + TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2.0f; + hintY = centerY + + TypefaceUtils.getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; paint.setTextAlign(Align.LEFT); } else if (key.hasShiftedLetterHint()) { // The hint label is placed at top-right corner of the key. Used mainly on tablet. hintX = keyWidth - mKeyShiftedLetterHintPadding - - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; + - TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; paint.getFontMetrics(mFontMetrics); hintY = -mFontMetrics.top; paint.setTextAlign(Align.CENTER); } else { // key.hasHintLetter() // The hint letter is placed at top-right corner of the key. Used mainly on phone. hintX = keyWidth - mKeyHintLetterPadding - - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2; + - TypefaceUtils.getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) + / 2.0f; hintY = -paint.ascent(); paint.setTextAlign(Align.CENTER); } @@ -506,7 +513,7 @@ public class KeyboardView extends View { paint.setColor(params.mHintLabelColor); paint.setTextAlign(Align.CENTER); final float hintX = keyWidth - mKeyHintLetterPadding - - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; + - TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; final float hintY = keyHeight - mKeyPopupHintLetterPadding; canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint); @@ -517,54 +524,6 @@ public class KeyboardView extends View { } } - private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) { - final int labelSize = (int)paint.getTextSize(); - final Typeface face = paint.getTypeface(); - final int codePointOffset = referenceChar << 15; - if (face == Typeface.DEFAULT) { - return codePointOffset + labelSize; - } else if (face == Typeface.DEFAULT_BOLD) { - return codePointOffset + labelSize + 0x1000; - } else if (face == Typeface.MONOSPACE) { - return codePointOffset + labelSize + 0x2000; - } else { - return codePointOffset + labelSize; - } - } - - // Working variable for the following methods. - private final Rect mTextBounds = new Rect(); - - private float getCharHeight(final char[] referenceChar, final Paint paint) { - final int key = getCharGeometryCacheKey(referenceChar[0], paint); - final Float cachedValue = sTextHeightCache.get(key); - if (cachedValue != null) - return cachedValue; - - paint.getTextBounds(referenceChar, 0, 1, mTextBounds); - final float height = mTextBounds.height(); - sTextHeightCache.put(key, height); - return height; - } - - private float getCharWidth(final char[] referenceChar, final Paint paint) { - final int key = getCharGeometryCacheKey(referenceChar[0], paint); - final Float cachedValue = sTextWidthCache.get(key); - if (cachedValue != null) - return cachedValue; - - paint.getTextBounds(referenceChar, 0, 1, mTextBounds); - final float width = mTextBounds.width(); - sTextWidthCache.put(key, width); - return width; - } - - // TODO: Remove this method. - public float getLabelWidth(final String label, final Paint paint) { - paint.getTextBounds(label, 0, label.length(), mTextBounds); - return mTextBounds.width(); - } - protected static void drawIcon(final Canvas canvas, final Drawable icon, final int x, final int y, final int width, final int height) { canvas.translate(x, y); @@ -578,7 +537,7 @@ public class KeyboardView extends View { paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(1.0f); paint.setColor(color); - canvas.drawLine(0, y, w, y, paint); + canvas.drawLine(0.0f, y, w, y, paint); } private static void drawVerticalLine(final Canvas canvas, final float x, final float h, @@ -586,7 +545,7 @@ public class KeyboardView extends View { paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(1.0f); paint.setColor(color); - canvas.drawLine(x, 0, x, h, paint); + canvas.drawLine(x, 0.0f, x, h, paint); } private static void drawRectangle(final Canvas canvas, final float x, final float y, @@ -595,15 +554,20 @@ public class KeyboardView extends View { paint.setStrokeWidth(1.0f); paint.setColor(color); canvas.translate(x, y); - canvas.drawRect(0, 0, w, h, paint); + canvas.drawRect(0.0f, 0.0f, w, h, paint); canvas.translate(-x, -y); } - public Paint newDefaultLabelPaint() { + public Paint newLabelPaint(final Key key) { final Paint paint = new Paint(); paint.setAntiAlias(true); - paint.setTypeface(mKeyDrawParams.mTypeface); - paint.setTextSize(mKeyDrawParams.mLabelSize); + if (key == null) { + paint.setTypeface(mKeyDrawParams.mTypeface); + paint.setTextSize(mKeyDrawParams.mLabelSize); + } else { + paint.setTypeface(key.selectTypeface(mKeyDrawParams)); + paint.setTextSize(key.selectTextSize(mKeyDrawParams)); + } return paint; } diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 745e7dfed..ba78d014a 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -530,9 +530,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0); final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_keyHysteresisDistance, 0); + R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f); final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0); + R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f); mKeyDetector = new KeyDetector( keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier); mKeyTimerHandler = new KeyTimerHandler(this, mainKeyboardViewAttr); @@ -655,7 +655,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mKeyTimerHandler.cancelLongPressTimer(); super.setKeyboard(keyboard); mKeyDetector.setKeyboard( - keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection); + keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection()); PointerTracker.setKeyDetector(mKeyDetector); mTouchScreenRegulator.setKeyboardGeometry(keyboard.mOccupiedWidth); mMoreKeysKeyboardCache.clear(); @@ -1329,7 +1329,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack // Overlay a dark rectangle to dim. if (mNeedsToDimEntireKeyboard) { - canvas.drawRect(0, 0, getWidth(), getHeight(), mBackgroundDimAlphaPaint); + canvas.drawRect(0.0f, 0.0f, getWidth(), getHeight(), mBackgroundDimAlphaPaint); } } @@ -1353,9 +1353,10 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } } - private boolean fitsTextIntoWidth(final int width, final String text, final Paint paint) { + private static boolean fitsTextIntoWidth(final int width, final String text, + final Paint paint) { paint.setTextScaleX(1.0f); - final float textWidth = getLabelWidth(text, paint); + final float textWidth = TypefaceUtils.getLabelWidth(text, paint); if (textWidth < width) { return true; } @@ -1366,12 +1367,12 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } paint.setTextScaleX(scaleX); - return getLabelWidth(text, paint) < width; + return TypefaceUtils.getLabelWidth(text, paint) < width; } // Layout language name on spacebar. - private String layoutLanguageOnSpacebar(final Paint paint, final InputMethodSubtype subtype, - final int width) { + private static String layoutLanguageOnSpacebar(final Paint paint, + final InputMethodSubtype subtype, final int width) { // Choose appropriate language name to fit into the width. final String fullText = getFullDisplayName(subtype); if (fitsTextIntoWidth(width, fullText, paint)) { @@ -1460,7 +1461,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack return ""; } final Locale locale = SubtypeLocale.getSubtypeLocale(subtype); - return StringUtils.toTitleCase(locale.getLanguage(), locale); + return StringUtils.capitalizeFirstCodePoint(locale.getLanguage(), locale); } // Get InputMethodSubtype's middle display name in its locale. diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java index 66c30149c..ae08a5953 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java @@ -17,6 +17,7 @@ package com.android.inputmethod.keyboard; import android.content.Context; +import android.content.res.Resources; import android.graphics.Paint; import android.graphics.drawable.Drawable; @@ -73,10 +74,11 @@ public final class MoreKeysKeyboard extends Keyboard { final int rowHeight, final int coordXInParent, final int parentKeyboardWidth, final boolean isFixedColumnOrder, final int dividerWidth) { mIsFixedOrder = isFixedColumnOrder; - if (parentKeyboardWidth / keyWidth < maxColumns) { + if (parentKeyboardWidth / keyWidth < Math.min(numKeys, maxColumns)) { throw new IllegalArgumentException( "Keyboard is too small to hold more keys keyboard: " - + parentKeyboardWidth + " " + keyWidth + " " + maxColumns); + + parentKeyboardWidth + " " + keyWidth + " " + + numKeys + " " + maxColumns); } mDefaultKeyWidth = keyWidth; mDefaultRowHeight = rowHeight; @@ -257,7 +259,6 @@ public final class MoreKeysKeyboard extends Keyboard { private static final float LABEL_PADDING_RATIO = 0.2f; private static final float DIVIDER_RATIO = 0.2f; - /** * The builder of MoreKeysKeyboard. * @param context the context of {@link MoreKeysKeyboardView}. @@ -289,11 +290,23 @@ public final class MoreKeysKeyboard extends Keyboard { // be considered because the vertical positions of both backgrounds were already // adjusted with their bottom paddings deducted. width = keyPreviewDrawParams.mPreviewVisibleWidth; - height = keyPreviewDrawParams.mPreviewVisibleHeight - + mParams.mVerticalGap; + height = keyPreviewDrawParams.mPreviewVisibleHeight + mParams.mVerticalGap; + // TODO: Remove this check. + if (width == 0) { + throw new IllegalArgumentException( + "Zero width key detected: " + parentKey + " in " + parentKeyboard.mId); + } } else { - width = getMaxKeyWidth(parentKeyboardView, parentKey, mParams.mDefaultKeyWidth); + width = getMaxKeyWidth(parentKeyboardView, parentKey, mParams.mDefaultKeyWidth, + context.getResources()); height = parentKeyboard.mMostCommonKeyHeight; + // TODO: Remove this check. + if (width == 0) { + throw new IllegalArgumentException( + "Zero width calculated: " + parentKey + + " moreKeys=" + java.util.Arrays.toString(parentKey.mMoreKeys) + + " in " + parentKeyboard.mId); + } } final int dividerWidth; if (parentKey.needsDividersInMoreKeys()) { @@ -310,22 +323,18 @@ public final class MoreKeysKeyboard extends Keyboard { } private static int getMaxKeyWidth(final KeyboardView view, final Key parentKey, - final int minKeyWidth) { - final int padding = (int)(view.getResources() - .getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding) - + (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0)); - final Paint paint = view.newDefaultLabelPaint(); - paint.setTypeface(parentKey.selectTypeface(view.mKeyDrawParams)); - paint.setTextSize(parentKey.selectMoreKeyTextSize(view.mKeyDrawParams)); + final int minKeyWidth, final Resources res) { + final float padding = + res.getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding) + + (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0.0f); + final Paint paint = view.newLabelPaint(parentKey); int maxWidth = minKeyWidth; for (final MoreKeySpec spec : parentKey.mMoreKeys) { final String label = spec.mLabel; // If the label is single letter, minKeyWidth is enough to hold the label. if (label != null && StringUtils.codePointCount(label) > 1) { - final int width = (int)view.getLabelWidth(label, paint) + padding; - if (maxWidth < width) { - maxWidth = width; - } + maxWidth = Math.max(maxWidth, + (int)(TypefaceUtils.getLabelWidth(label, paint) + padding)); } } return maxWidth; diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java index 0d42ab2fe..a82fb79bd 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java @@ -33,7 +33,7 @@ import com.android.inputmethod.latin.R; public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel { private final int[] mCoordinates = CoordinateUtils.newInstance(); - private final KeyDetector mKeyDetector; + protected final KeyDetector mKeyDetector; private Controller mController; protected KeyboardActionListener mListener; private int mOriginX; @@ -71,7 +71,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel public void setKeyboard(final Keyboard keyboard) { super.setKeyboard(keyboard); mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), - -getPaddingTop() + mVerticalCorrection); + -getPaddingTop() + getVerticalCorrection()); } @Override diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index dcfae739e..91c4319e7 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -441,10 +441,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element { // Returns true if keyboard has been changed by this callback. private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) { - if (sInGesture) { + if (sInGesture || mIsDetectingGesture) { return false; } - final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier(); + final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onPress : %s%s%s", mPointerId, KeyDetector.printableCode(key), @@ -468,7 +468,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { // primaryCode is different from {@link Key#mCode}. private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x, final int y, final long eventTime) { - final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier(); + final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState(); final int code = altersCode ? key.getAltCode() : primaryCode; if (DEBUG_LISTENER) { @@ -500,10 +500,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element { // primaryCode is different from {@link Key#mCode}. private void callListenerOnRelease(final Key key, final int primaryCode, final boolean withSliding) { - if (sInGesture) { + if (sInGesture || mIsDetectingGesture) { return; } - final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier(); + final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onRelease : %s%s%s%s", mPointerId, Constants.printableCode(primaryCode), diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java index 32cee734a..b77e378bf 100644 --- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java +++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java @@ -26,7 +26,7 @@ import com.android.inputmethod.latin.JniUtils; import java.util.Arrays; -public final class ProximityInfo { +public class ProximityInfo { private static final String TAG = ProximityInfo.class.getSimpleName(); private static final boolean DEBUG = false; @@ -79,22 +79,21 @@ public final class ProximityInfo { mNativeProximityInfo = createNativeProximityInfo(touchPositionCorrection); } - private static ProximityInfo createDummyProximityInfo() { - return new ProximityInfo("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null); - } - - public static ProximityInfo createSpellCheckerProximityInfo(final int[] proximityCharsArray, - final int rowSize, final int gridWidth, final int gridHeight) { - final ProximityInfo spellCheckerProximityInfo = createDummyProximityInfo(); - spellCheckerProximityInfo.mNativeProximityInfo = - spellCheckerProximityInfo.setProximityInfoNative("" /* locale */, - gridWidth /* displayWidth */, gridHeight /* displayHeight */, - gridWidth, gridHeight, 1 /* mostCommonKeyWidth */, proximityCharsArray, - 0 /* keyCount */, null /*keyXCoordinates */, null /* keyYCoordinates */, - null /* keyWidths */, null /* keyHeights */, null /* keyCharCodes */, - null /* sweetSpotCenterXs */, null /* sweetSpotCenterYs */, - null /* sweetSpotRadii */); - return spellCheckerProximityInfo; + /** + * Constructor for subclasses such as + * {@link com.android.inputmethod.latin.spellcheck.SpellCheckerProximityInfo}. + */ + protected ProximityInfo(final int[] proximityCharsArray, final int gridWidth, + final int gridHeight) { + this("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null); + mNativeProximityInfo = setProximityInfoNative("" /* locale */, + gridWidth /* displayWidth */, gridHeight /* displayHeight */, + gridWidth, gridHeight, 1 /* mostCommonKeyWidth */, + 1 /* mostCommonKeyHeight */, proximityCharsArray, 0 /* keyCount */, + null /*keyXCoordinates */, null /* keyYCoordinates */, + null /* keyWidths */, null /* keyHeights */, null /* keyCharCodes */, + null /* sweetSpotCenterXs */, null /* sweetSpotCenterYs */, + null /* sweetSpotRadii */); } private long mNativeProximityInfo; @@ -105,9 +104,10 @@ public final class ProximityInfo { // TODO: Stop passing proximityCharsArray private static native long setProximityInfoNative(String locale, int displayWidth, int displayHeight, int gridWidth, int gridHeight, - int mostCommonKeyWidth, int[] proximityCharsArray, int keyCount, int[] keyXCoordinates, - int[] keyYCoordinates, int[] keyWidths, int[] keyHeights, int[] keyCharCodes, - float[] sweetSpotCenterXs, float[] sweetSpotCenterYs, float[] sweetSpotRadii); + int mostCommonKeyWidth, int mostCommonKeyHeight, int[] proximityCharsArray, + int keyCount, int[] keyXCoordinates, int[] keyYCoordinates, int[] keyWidths, + int[] keyHeights, int[] keyCharCodes, float[] sweetSpotCenterXs, + float[] sweetSpotCenterYs, float[] sweetSpotRadii); private static native void releaseProximityInfoNative(long nativeProximityInfo); @@ -234,9 +234,9 @@ public final class ProximityInfo { // TODO: Stop passing proximityCharsArray return setProximityInfoNative(mLocaleStr, mKeyboardMinWidth, mKeyboardHeight, - mGridWidth, mGridHeight, mMostCommonKeyWidth, proximityCharsArray, keyCount, - keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes, - sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii); + mGridWidth, mGridHeight, mMostCommonKeyWidth, mMostCommonKeyHeight, + proximityCharsArray, keyCount, keyXCoordinates, keyYCoordinates, keyWidths, + keyHeights, keyCharCodes, sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii); } public long getNativeProximityInfo() { diff --git a/java/src/com/android/inputmethod/keyboard/TypefaceUtils.java b/java/src/com/android/inputmethod/keyboard/TypefaceUtils.java new file mode 100644 index 000000000..6a54e119c --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/TypefaceUtils.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2013 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.keyboard; + +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.util.SparseArray; + +import com.android.inputmethod.latin.CollectionUtils; + +public final class TypefaceUtils { + private TypefaceUtils() { + // This utility class is not publicly instantiable. + } + + // This sparse array caches key label text height in pixel indexed by key label text size. + private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray(); + // Working variable for the following method. + private static final Rect sTextHeightBounds = new Rect(); + + public static float getCharHeight(final char[] referenceChar, final Paint paint) { + final int key = getCharGeometryCacheKey(referenceChar[0], paint); + synchronized (sTextHeightCache) { + final Float cachedValue = sTextHeightCache.get(key); + if (cachedValue != null) { + return cachedValue; + } + + paint.getTextBounds(referenceChar, 0, 1, sTextHeightBounds); + final float height = sTextHeightBounds.height(); + sTextHeightCache.put(key, height); + return height; + } + } + + // This sparse array caches key label text width in pixel indexed by key label text size. + private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray(); + // Working variable for the following method. + private static final Rect sTextWidthBounds = new Rect(); + + public static float getCharWidth(final char[] referenceChar, final Paint paint) { + final int key = getCharGeometryCacheKey(referenceChar[0], paint); + synchronized (sTextWidthCache) { + final Float cachedValue = sTextWidthCache.get(key); + if (cachedValue != null) { + return cachedValue; + } + + paint.getTextBounds(referenceChar, 0, 1, sTextWidthBounds); + final float width = sTextWidthBounds.width(); + sTextWidthCache.put(key, width); + return width; + } + } + + private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) { + final int labelSize = (int)paint.getTextSize(); + final Typeface face = paint.getTypeface(); + final int codePointOffset = referenceChar << 15; + if (face == Typeface.DEFAULT) { + return codePointOffset + labelSize; + } else if (face == Typeface.DEFAULT_BOLD) { + return codePointOffset + labelSize + 0x1000; + } else if (face == Typeface.MONOSPACE) { + return codePointOffset + labelSize + 0x2000; + } else { + return codePointOffset + labelSize; + } + } + + public static float getLabelWidth(final String label, final Paint paint) { + final Rect textBounds = new Rect(); + paint.getTextBounds(label, 0, label.length(), textBounds); + return textBounds.width(); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java index e3e6d39e4..0c050610d 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java @@ -37,6 +37,7 @@ import com.android.inputmethod.latin.ResizableIntArray; final class GesturePreviewTrail { private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY; + // These three {@link ResizableIntArray}s should be synchronized by {@link #mEventTimes}. private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY); @@ -50,6 +51,9 @@ final class GesturePreviewTrail { public final int mTrailColor; public final float mTrailStartWidth; public final float mTrailEndWidth; + public final float mTrailBodyRatio; + public boolean mTrailShadowEnabled; + public final float mTrailShadowRatio; public final int mFadeoutStartDelay; public final int mFadeoutDuration; public final int mUpdateInterval; @@ -63,6 +67,14 @@ final class GesturePreviewTrail { R.styleable.MainKeyboardView_gesturePreviewTrailStartWidth, 0.0f); mTrailEndWidth = mainKeyboardViewAttr.getDimension( R.styleable.MainKeyboardView_gesturePreviewTrailEndWidth, 0.0f); + final int PERCENTAGE_INT = 100; + mTrailBodyRatio = (float)mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gesturePreviewTrailBodyRatio, PERCENTAGE_INT) + / (float)PERCENTAGE_INT; + final int trailShadowRatioInt = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gesturePreviewTrailShadowRatio, 0); + mTrailShadowEnabled = (trailShadowRatioInt > 0); + mTrailShadowRatio = (float)trailShadowRatioInt / (float)PERCENTAGE_INT; mFadeoutStartDelay = mainKeyboardViewAttr.getInt( R.styleable.MainKeyboardView_gesturePreviewTrailFadeoutStartDelay, 0); mFadeoutDuration = mainKeyboardViewAttr.getInt( @@ -90,7 +102,13 @@ final class GesturePreviewTrail { } public void addStroke(final GestureStrokeWithPreviewPoints stroke, final long downTime) { - final int trailSize = mEventTimes.getLength(); + synchronized (mEventTimes) { + addStrokeLocked(stroke, downTime); + } + } + + private void addStrokeLocked(final GestureStrokeWithPreviewPoints stroke, final long downTime) { + final int trailSize = mEventTimes.getLength(); stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates); if (mEventTimes.getLength() == trailSize) { return; @@ -169,6 +187,13 @@ final class GesturePreviewTrail { */ public boolean drawGestureTrail(final Canvas canvas, final Paint paint, final Rect outBoundsRect, final Params params) { + synchronized (mEventTimes) { + return drawGestureTrailLocked(canvas, paint, outBoundsRect, params); + } + } + + private boolean drawGestureTrailLocked(final Canvas canvas, final Paint paint, + final Rect outBoundsRect, final Params params) { // Initialize bounds rectangle. outBoundsRect.setEmpty(); final int trailSize = mEventTimes.getLength(); @@ -205,14 +230,22 @@ final class GesturePreviewTrail { final float r2 = getWidth(elapsedTime, params) / 2.0f; // Draw trail line only when the current point isn't a down point. if (!isDownEventXCoord(xCoords[i])) { - final Path path = roundedLine.makePath(p1x, p1y, r1, p2x, p2y, r2); + final float body1 = r1 * params.mTrailBodyRatio; + final float body2 = r2 * params.mTrailBodyRatio; + final Path path = roundedLine.makePath(p1x, p1y, body1, p2x, p2y, body2); if (path != null) { + roundedLine.getBounds(mRoundedLineBounds); + if (params.mTrailShadowEnabled) { + final float shadow2 = r2 * params.mTrailShadowRatio; + paint.setShadowLayer(shadow2, 0.0f, 0.0f, params.mTrailColor); + final int shadowInset = -(int)Math.ceil(shadow2); + mRoundedLineBounds.inset(shadowInset, shadowInset); + } + // Take union for the bounds. + outBoundsRect.union(mRoundedLineBounds); final int alpha = getAlpha(elapsedTime, params); paint.setAlpha(alpha); canvas.drawPath(path, paint); - // Take union for the bounds. - roundedLine.getBounds(mRoundedLineBounds); - outBoundsRect.union(mRoundedLineBounds); } } p1x = p2x; diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java index 95d9ccb58..b1d499702 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java @@ -20,6 +20,7 @@ import android.text.TextUtils; import android.util.Log; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.RecapitalizeStatus; /** * Keyboard state machine. @@ -29,7 +30,7 @@ import com.android.inputmethod.latin.Constants; * The input events are {@link #onLoadKeyboard()}, {@link #onSaveKeyboardState()}, * {@link #onPressKey(int, boolean, int)}, {@link #onReleaseKey(int, boolean)}, * {@link #onCodeInput(int, boolean, int)}, {@link #onCancelInput(boolean)}, - * {@link #onUpdateShiftState(int)}, {@link #onLongPressTimeout(int)}. + * {@link #onUpdateShiftState(int, int)}, {@link #onLongPressTimeout(int)}. * * The actions are {@link SwitchActions}'s methods. */ @@ -48,7 +49,7 @@ public final class KeyboardState { public void setSymbolsShiftedKeyboard(); /** - * Request to call back {@link KeyboardState#onUpdateShiftState(int)}. + * Request to call back {@link KeyboardState#onUpdateShiftState(int, int)}. */ public void requestUpdatingShiftState(); @@ -80,6 +81,7 @@ public final class KeyboardState { private boolean mIsSymbolShifted; private boolean mPrevMainKeyboardWasShiftLocked; private boolean mPrevSymbolsKeyboardWasShifted; + private int mRecapitalizeMode; // For handling long press. private boolean mLongPressShiftLockFired; @@ -110,6 +112,7 @@ public final class KeyboardState { public KeyboardState(final SwitchActions switchActions) { mSwitchActions = switchActions; + mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; } public void onLoadKeyboard() { @@ -283,6 +286,7 @@ public final class KeyboardState { mSwitchActions.setAlphabetKeyboard(); mIsAlphabetMode = true; mIsSymbolShifted = false; + mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; mSwitchState = SWITCH_STATE_ALPHA; mSwitchActions.requestUpdatingShiftState(); } @@ -386,11 +390,13 @@ public final class KeyboardState { } } - public void onUpdateShiftState(final int autoCaps) { + public void onUpdateShiftState(final int autoCaps, final int recapitalizeMode) { if (DEBUG_EVENT) { - Log.d(TAG, "onUpdateShiftState: autoCaps=" + autoCaps + " " + this); + Log.d(TAG, "onUpdateShiftState: autoCaps=" + autoCaps + ", recapitalizeMode=" + + recapitalizeMode + " " + this); } - updateAlphabetShiftState(autoCaps); + mRecapitalizeMode = recapitalizeMode; + updateAlphabetShiftState(autoCaps, recapitalizeMode); } // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout @@ -402,8 +408,28 @@ public final class KeyboardState { resetKeyboardStateToAlphabet(); } - private void updateAlphabetShiftState(final int autoCaps) { + private void updateShiftStateForRecapitalize(final int recapitalizeMode) { + switch (recapitalizeMode) { + case RecapitalizeStatus.CAPS_MODE_ALL_UPPER: + setShifted(SHIFT_LOCK_SHIFTED); + break; + case RecapitalizeStatus.CAPS_MODE_FIRST_WORD_UPPER: + setShifted(AUTOMATIC_SHIFT); + break; + case RecapitalizeStatus.CAPS_MODE_ALL_LOWER: + case RecapitalizeStatus.CAPS_MODE_ORIGINAL_MIXED_CASE: + default: + setShifted(UNSHIFT); + } + } + + private void updateAlphabetShiftState(final int autoCaps, final int recapitalizeMode) { if (!mIsAlphabetMode) return; + if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != recapitalizeMode) { + // We are recapitalizing. Match the keyboard to the current recapitalize state. + updateShiftStateForRecapitalize(recapitalizeMode); + return; + } if (!mShiftKeyState.isReleasing()) { // Ignore update shift state event while the shift key is being pressed (including // chording). @@ -421,6 +447,9 @@ public final class KeyboardState { private void onPressShift() { mLongPressShiftLockFired = false; + // If we are recapitalizing, we don't do any of the normal processing, including + // importantly the double tap timer. + if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) return; if (mIsAlphabetMode) { mIsInDoubleTapShiftKey = mSwitchActions.isInDoubleTapTimeout(); if (!mIsInDoubleTapShiftKey) { @@ -467,7 +496,11 @@ public final class KeyboardState { } private void onReleaseShift(final boolean withSliding) { - if (mIsAlphabetMode) { + if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) { + // We are recapitalizing. We should match the keyboard state to the recapitalize + // state in priority. + updateShiftStateForRecapitalize(mRecapitalizeMode); + } else if (mIsAlphabetMode) { final boolean isShiftLocked = mAlphabetShiftState.isShiftLocked(); mIsInAlphabetUnshiftedFromShifted = false; if (mIsInDoubleTapShiftKey) { @@ -597,7 +630,7 @@ public final class KeyboardState { // If the code is a letter, update keyboard shift state. if (Constants.isLetterCode(code)) { - updateAlphabetShiftState(autoCaps); + updateAlphabetShiftState(autoCaps, RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java index 2df7e5cf5..6bc6acc0f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java +++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java @@ -34,175 +34,197 @@ public final class PointerTrackerQueue { } private static final int INITIAL_CAPACITY = 10; + // Note: {@link #mExpandableArrayOfActivePointers} and {@link #mArraySize} are synchronized by + // {@link #mExpandableArrayOfActivePointers} private final ArrayList<Element> mExpandableArrayOfActivePointers = CollectionUtils.newArrayList(INITIAL_CAPACITY); private int mArraySize = 0; - public synchronized int size() { - return mArraySize; + public int size() { + synchronized (mExpandableArrayOfActivePointers) { + return mArraySize; + } } - public synchronized void add(final Element pointer) { - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - if (arraySize < expandableArray.size()) { - expandableArray.set(arraySize, pointer); - } else { - expandableArray.add(pointer); + public void add(final Element pointer) { + synchronized (mExpandableArrayOfActivePointers) { + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + if (arraySize < expandableArray.size()) { + expandableArray.set(arraySize, pointer); + } else { + expandableArray.add(pointer); + } + mArraySize = arraySize + 1; } - mArraySize = arraySize + 1; } - public synchronized void remove(final Element pointer) { - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - int newSize = 0; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element == pointer) { + public void remove(final Element pointer) { + synchronized (mExpandableArrayOfActivePointers) { + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + int newSize = 0; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element == pointer) { + if (newSize != index) { + Log.w(TAG, "Found duplicated element in remove: " + pointer); + } + continue; // Remove this element from the expandableArray. + } if (newSize != index) { - Log.w(TAG, "Found duplicated element in remove: " + pointer); + // Shift this element toward the beginning of the expandableArray. + expandableArray.set(newSize, element); } - continue; // Remove this element from the expandableArray. - } - if (newSize != index) { - // Shift this element toward the beginning of the expandableArray. - expandableArray.set(newSize, element); + newSize++; } - newSize++; + mArraySize = newSize; } - mArraySize = newSize; } - public synchronized Element getOldestElement() { - return (mArraySize == 0) ? null : mExpandableArrayOfActivePointers.get(0); + public Element getOldestElement() { + synchronized (mExpandableArrayOfActivePointers) { + return (mArraySize == 0) ? null : mExpandableArrayOfActivePointers.get(0); + } } - public synchronized void releaseAllPointersOlderThan(final Element pointer, - final long eventTime) { - if (DEBUG) { - Log.d(TAG, "releaseAllPoniterOlderThan: " + pointer + " " + this); - } - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - int newSize, index; - for (newSize = index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element == pointer) { - break; // Stop releasing elements. - } - if (!element.isModifier()) { - element.onPhantomUpEvent(eventTime); - continue; // Remove this element from the expandableArray. + public void releaseAllPointersOlderThan(final Element pointer, final long eventTime) { + synchronized (mExpandableArrayOfActivePointers) { + if (DEBUG) { + Log.d(TAG, "releaseAllPoniterOlderThan: " + pointer + " " + this); } - if (newSize != index) { - // Shift this element toward the beginning of the expandableArray. - expandableArray.set(newSize, element); - } - newSize++; - } - // Shift rest of the expandableArray. - int count = 0; - for (; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element == pointer) { - if (count > 0) { - Log.w(TAG, "Found duplicated element in releaseAllPointersOlderThan: " - + pointer); + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + int newSize, index; + for (newSize = index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element == pointer) { + break; // Stop releasing elements. + } + if (!element.isModifier()) { + element.onPhantomUpEvent(eventTime); + continue; // Remove this element from the expandableArray. + } + if (newSize != index) { + // Shift this element toward the beginning of the expandableArray. + expandableArray.set(newSize, element); } - count++; - } - if (newSize != index) { - expandableArray.set(newSize, expandableArray.get(index)); newSize++; } + // Shift rest of the expandableArray. + int count = 0; + for (; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element == pointer) { + if (count > 0) { + Log.w(TAG, "Found duplicated element in releaseAllPointersOlderThan: " + + pointer); + } + count++; + } + if (newSize != index) { + expandableArray.set(newSize, expandableArray.get(index)); + newSize++; + } + } + mArraySize = newSize; } - mArraySize = newSize; } public void releaseAllPointers(final long eventTime) { releaseAllPointersExcept(null, eventTime); } - public synchronized void releaseAllPointersExcept(final Element pointer, - final long eventTime) { - if (DEBUG) { - if (pointer == null) { - Log.d(TAG, "releaseAllPoniters: " + this); - } else { - Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this); - } - } - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - int newSize = 0, count = 0; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element == pointer) { - if (count > 0) { - Log.w(TAG, "Found duplicated element in releaseAllPointersExcept: " + pointer); + public void releaseAllPointersExcept(final Element pointer, final long eventTime) { + synchronized (mExpandableArrayOfActivePointers) { + if (DEBUG) { + if (pointer == null) { + Log.d(TAG, "releaseAllPoniters: " + this); + } else { + Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this); } - count++; - } else { - element.onPhantomUpEvent(eventTime); - continue; // Remove this element from the expandableArray. } - if (newSize != index) { - // Shift this element toward the beginning of the expandableArray. - expandableArray.set(newSize, element); + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + int newSize = 0, count = 0; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element == pointer) { + if (count > 0) { + Log.w(TAG, "Found duplicated element in releaseAllPointersExcept: " + + pointer); + } + count++; + } else { + element.onPhantomUpEvent(eventTime); + continue; // Remove this element from the expandableArray. + } + if (newSize != index) { + // Shift this element toward the beginning of the expandableArray. + expandableArray.set(newSize, element); + } + newSize++; } - newSize++; + mArraySize = newSize; } - mArraySize = newSize; } - public synchronized boolean hasModifierKeyOlderThan(final Element pointer) { - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element == pointer) { - return false; // Stop searching modifier key. - } - if (element.isModifier()) { - return true; + public boolean hasModifierKeyOlderThan(final Element pointer) { + synchronized (mExpandableArrayOfActivePointers) { + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element == pointer) { + return false; // Stop searching modifier key. + } + if (element.isModifier()) { + return true; + } } + return false; } - return false; } - public synchronized boolean isAnyInSlidingKeyInput() { - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (element.isInSlidingKeyInput()) { - return true; + public boolean isAnyInSlidingKeyInput() { + synchronized (mExpandableArrayOfActivePointers) { + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (element.isInSlidingKeyInput()) { + return true; + } } + return false; } - return false; } - public synchronized void cancelAllPointerTracker() { - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - element.cancelTracking(); + public void cancelAllPointerTracker() { + synchronized (mExpandableArrayOfActivePointers) { + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + element.cancelTracking(); + } } } @Override - public synchronized String toString() { - final StringBuilder sb = new StringBuilder(); - final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; - final int arraySize = mArraySize; - for (int index = 0; index < arraySize; index++) { - final Element element = expandableArray.get(index); - if (sb.length() > 0) - sb.append(" "); - sb.append(element.toString()); + public String toString() { + synchronized (mExpandableArrayOfActivePointers) { + final StringBuilder sb = new StringBuilder(); + final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; + final int arraySize = mArraySize; + for (int index = 0; index < arraySize; index++) { + final Element element = expandableArray.get(index); + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(element.toString()); + } + return "[" + sb.toString() + "]"; } - return "[" + sb.toString() + "]"; } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java b/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java index 37f4e3582..33dbbafa3 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java +++ b/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java @@ -30,7 +30,7 @@ import com.android.inputmethod.latin.R; * Draw rubber band preview graphics during sliding key input. */ public final class SlidingKeyInputPreview extends AbstractDrawingPreview { - private final int mPreviewWidth; + private final float mPreviewBodyRadius; private boolean mShowSlidingKeyInputPreview; private final int[] mPreviewFrom = CoordinateUtils.newInstance(); @@ -44,8 +44,20 @@ public final class SlidingKeyInputPreview extends AbstractDrawingPreview { super(drawingView); final int previewColor = mainKeyboardViewAttr.getColor( R.styleable.MainKeyboardView_slidingKeyInputPreviewColor, 0); - mPreviewWidth = mainKeyboardViewAttr.getDimensionPixelSize( - R.styleable.MainKeyboardView_slidingKeyInputPreviewWidth, 0); + final float previewRadius = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_slidingKeyInputPreviewWidth, 0) / 2.0f; + final int PERCENTAGE_INT = 100; + final float previewBodyRatio = (float)mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_slidingKeyInputPreviewBodyRatio, PERCENTAGE_INT) + / (float)PERCENTAGE_INT; + mPreviewBodyRadius = previewRadius * previewBodyRatio; + final int previewShadowRatioInt = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_slidingKeyInputPreviewShadowRatio, 0); + if (previewShadowRatioInt > 0) { + final float previewShadowRatio = (float)previewShadowRatioInt / (float)PERCENTAGE_INT; + final float shadowRadius = previewRadius * previewShadowRatio; + mPaint.setShadowLayer(shadowRadius, 0.0f, 0.0f, previewColor); + } mPaint.setColor(previewColor); } @@ -65,7 +77,7 @@ public final class SlidingKeyInputPreview extends AbstractDrawingPreview { } // TODO: Finalize the rubber band preview implementation. - final int radius = mPreviewWidth / 2; + final float radius = mPreviewBodyRadius; final Path path = mRoundedLine.makePath( CoordinateUtils.x(mPreviewFrom), CoordinateUtils.y(mPreviewFrom), radius, CoordinateUtils.x(mPreviewTo), CoordinateUtils.y(mPreviewTo), radius); diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index 562e1d0b7..42f713697 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -422,7 +422,7 @@ public final class BinaryDictionaryFileDumper { private static void reinitializeClientRecordInDictionaryContentProvider(final Context context, final ContentProviderClient client, final String clientId) throws RemoteException { - final String metadataFileUri = context.getString(R.string.dictionary_pack_metadata_uri); + final String metadataFileUri = MetadataFileUriGetter.getMetadataUri(context); if (TextUtils.isEmpty(metadataFileUri)) return; // Tell the content provider to reset all information about this client id final Uri metadataContentUri = getProviderUriBuilder(clientId) diff --git a/java/src/com/android/inputmethod/latin/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/CapsModeUtils.java index 1012cd519..4b8d1ac11 100644 --- a/java/src/com/android/inputmethod/latin/CapsModeUtils.java +++ b/java/src/com/android/inputmethod/latin/CapsModeUtils.java @@ -41,7 +41,7 @@ public final class CapsModeUtils { if (WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == capitalizeMode) { return s.toUpperCase(locale); } else if (WordComposer.CAPS_MODE_AUTO_SHIFTED == capitalizeMode) { - return StringUtils.toTitleCase(s, locale); + return StringUtils.capitalizeFirstCodePoint(s, locale); } else { return s; } diff --git a/java/src/com/android/inputmethod/latin/CompletionInfoUtils.java b/java/src/com/android/inputmethod/latin/CompletionInfoUtils.java new file mode 100644 index 000000000..792a446c9 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/CompletionInfoUtils.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2013 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.text.TextUtils; +import android.view.inputmethod.CompletionInfo; + +import java.util.Arrays; + +/** + * Utilities to do various stuff with CompletionInfo. + */ +public class CompletionInfoUtils { + private CompletionInfoUtils() { + // This utility class is not publicly instantiable. + } + + public static CompletionInfo[] removeNulls(final CompletionInfo[] src) { + int j = 0; + final CompletionInfo[] dst = new CompletionInfo[src.length]; + for (int i = 0; i < src.length; ++i) { + if (null != src[i] && !TextUtils.isEmpty(src[i].getText())) { + dst[j] = src[i]; + ++j; + } + } + return Arrays.copyOfRange(dst, 0, j); + } +} diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 50e50233e..86bb25562 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -160,6 +160,8 @@ public final class Constants { public static final int CODE_DOUBLE_QUOTE = '"'; public static final int CODE_QUESTION_MARK = '?'; public static final int CODE_EXCLAMATION_MARK = '!'; + public static final int CODE_SLASH = '/'; + public static final int CODE_COMMERCIAL_AT = '@'; // TODO: Check how this should work for right-to-left languages. It seems to stand // that for rtl languages, a closing parenthesis is a left parenthesis. Is this // managed by the font? Or is it a different char? diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java index 8b5a76a17..75c2cf2c8 100644 --- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java @@ -173,7 +173,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { // capitalization of i. final int wordLen = StringUtils.codePointCount(word); if (wordLen < MAX_WORD_LENGTH && wordLen > 1) { - super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS); + super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS, + false /* isNotAWord */); if (!TextUtils.isEmpty(prevWord)) { if (mUseFirstLastBigrams) { super.setBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM); @@ -251,7 +252,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { } private static boolean isValidName(final String name) { - if (name != null && -1 == name.indexOf('@')) { + if (name != null && -1 == name.indexOf(Constants.CODE_COMMERCIAL_AT)) { return true; } return false; diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index ff3d83fad..9691fa231 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -37,6 +37,8 @@ public abstract class Dictionary { public static final String TYPE_USER = "user"; // User history dictionary internal to LatinIME. public static final String TYPE_USER_HISTORY = "history"; + // Spawned by resuming suggestions. Comes from a span that was in the TextView. + public static final String TYPE_RESUMED = "resumed"; protected final String mDictType; public Dictionary(final String dictType) { diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index 97dc6a8ac..4b1975a00 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -176,14 +176,15 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries, // considering performance regression. - protected void addWord(final String word, final String shortcutTarget, final int frequency) { + protected void addWord(final String word, final String shortcutTarget, final int frequency, + final boolean isNotAWord) { if (shortcutTarget == null) { - mFusionDictionary.add(word, frequency, null, false /* isNotAWord */); + mFusionDictionary.add(word, frequency, null, isNotAWord); } else { // TODO: Do this in the subclass, with this class taking an arraylist. final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList(); shortcutTargets.add(new WeightedString(shortcutTarget, frequency)); - mFusionDictionary.add(word, frequency, shortcutTargets, false /* isNotAWord */); + mFusionDictionary.add(word, frequency, shortcutTargets, isNotAWord); } } diff --git a/java/src/com/android/inputmethod/latin/InputTypeUtils.java b/java/src/com/android/inputmethod/latin/InputTypeUtils.java index ecb20144b..46194f6e4 100644 --- a/java/src/com/android/inputmethod/latin/InputTypeUtils.java +++ b/java/src/com/android/inputmethod/latin/InputTypeUtils.java @@ -33,7 +33,6 @@ public final class InputTypeUtils implements InputType { private static final int[] SUPPRESSING_AUTO_SPACES_FIELD_VARIATION = { InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS, InputType.TYPE_TEXT_VARIATION_PASSWORD, - InputType.TYPE_TEXT_VARIATION_URI, InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD, InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD }; public static final int IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1; diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 56b1c786e..0e1c4dc31 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -44,7 +44,9 @@ import android.os.Message; import android.os.SystemClock; import android.preference.PreferenceManager; import android.text.InputType; +import android.text.SpannableString; import android.text.TextUtils; +import android.text.style.SuggestionSpan; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; @@ -72,6 +74,7 @@ import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.MainKeyboardView; +import com.android.inputmethod.latin.RichInputConnection.Range; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.Utils.Stats; import com.android.inputmethod.latin.define.ProductionFlag; @@ -158,6 +161,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction mPositionalInfoForUserDictPendingAddition = null; private final WordComposer mWordComposer = new WordComposer(); private final RichInputConnection mConnection = new RichInputConnection(this); + private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); // Keep track of the last selection range to decide if we need to show word alternatives private static final int NOT_A_CURSOR_POSITION = -1; @@ -197,6 +201,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction private static final int MSG_PENDING_IMS_CALLBACK = 1; private static final int MSG_UPDATE_SUGGESTION_STRIP = 2; private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; + private static final int MSG_RESUME_SUGGESTIONS = 4; private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; @@ -234,6 +239,9 @@ public final class LatinIME extends InputMethodService implements KeyboardAction latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj, msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); break; + case MSG_RESUME_SUGGESTIONS: + latinIme.restartSuggestionsOnWordTouchedByCursor(); + break; } } @@ -241,6 +249,10 @@ public final class LatinIME extends InputMethodService implements KeyboardAction sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions); } + public void postResumeSuggestions() { + sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions); + } + public void cancelUpdateSuggestionStrip() { removeMessages(MSG_UPDATE_SUGGESTION_STRIP); } @@ -730,6 +742,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction resetComposingState(true /* alsoResetLastComposedWord */); mDeleteCount = 0; mSpaceState = SPACE_STATE_NONE; + mRecapitalizeStatus.deactivate(); mCurrentlyPressedHardwareKeys.clear(); if (mSuggestionStripView != null) { @@ -910,13 +923,13 @@ public final class LatinIME extends InputMethodService implements KeyboardAction resetEntireInputState(newSelStart); } + // We moved the cursor. If we are touching a word, we need to resume suggestion. + mHandler.postResumeSuggestions(); + // Reset the last recapitalization. + mRecapitalizeStatus.deactivate(); mKeyboardSwitcher.updateShiftState(); } mExpectingUpdateSelection = false; - // TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not - // here. It would probably be too expensive to call directly here but we may want to post a - // message to delay it. The point would be to unify behavior between backspace to the - // end of a word and manually put the pointer at the end of the word. // Make a note of the cursor position mLastSelectionStart = newSelStart; @@ -983,7 +996,6 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } } if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return; - mApplicationSpecifiedCompletions = applicationSpecifiedCompletions; if (applicationSpecifiedCompletions == null) { clearSuggestionStrip(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { @@ -991,6 +1003,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } return; } + mApplicationSpecifiedCompletions = + CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions); final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords = SuggestedWords.getFromApplicationSpecifiedCompletions( @@ -1166,6 +1180,15 @@ public final class LatinIME extends InputMethodService implements KeyboardAction SPACE_STATE_PHANTOM == mSpaceState); } + public int getCurrentRecapitalizeState() { + if (!mRecapitalizeStatus.isActive() + || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { + // Not recapitalizing at the moment + return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; + } + return mRecapitalizeStatus.getCurrentMode(); + } + // Factor in auto-caps and manual caps and compute the current caps mode. private int getActualCapsMode() { final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode(); @@ -1243,10 +1266,6 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } else { wordToEdit = word; } - mPositionalInfoForUserDictPendingAddition = - new PositionalInfoForUserDictPendingAddition( - wordToEdit, mLastSelectionEnd, getCurrentInputEditorInfo(), - mLastComposedWord.mCapitalizedMode); mUserDictionary.addWordToUserDictionary(wordToEdit); } @@ -1380,8 +1399,18 @@ public final class LatinIME extends InputMethodService implements KeyboardAction LatinImeLogger.logOnDelete(x, y); break; case Constants.CODE_SHIFT: + // Note: calling back to the keyboard on Shift key is handled in onPressKey() + // and onReleaseKey(). + final Keyboard currentKeyboard = switcher.getKeyboard(); + if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { + // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for + // alphabetic shift and shift while in symbol layout. + handleRecapitalize(); + } + break; case Constants.CODE_SWITCH_ALPHA_SYMBOL: - // Shift and symbol key is handled in onPressKey() and onReleaseKey(). + // Note: calling back to the keyboard on symbol key is handled in onPressKey() + // and onReleaseKey(). break; case Constants.CODE_SETTINGS: onSettingsKeyPressed(); @@ -1459,7 +1488,13 @@ public final class LatinIME extends InputMethodService implements KeyboardAction "", mWordComposer.getTypedWord(), " ", mWordComposer); } } - commitTyped(LastComposedWord.NOT_A_SEPARATOR); + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the character at the current cursor position. + resetEntireInputState(mLastSelectionStart); + } else { + commitTyped(LastComposedWord.NOT_A_SEPARATOR); + } } final int keyX, keyY; final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); @@ -1515,8 +1550,12 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } final int wordComposerSize = mWordComposer.size(); // Since isComposingWord() is true, the size is at least 1. - final int lastChar = mWordComposer.getCodeAt(wordComposerSize - 1); - if (wordComposerSize <= 1) { + final int lastChar = mWordComposer.getCodeBeforeCursor(); + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the batch input at the current cursor position. + resetEntireInputState(mLastSelectionStart); + } else if (wordComposerSize <= 1) { // We auto-correct the previous (typed, not gestured) string iff it's one character // long. The reason for this is, even in the middle of gesture typing, you'll still // tap one-letter words and you want them auto-corrected (typically, "i" in English @@ -1540,7 +1579,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } } else { final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); - if (mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) { + if (Character.isLetter(codePointBeforeCursor) + || mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) { mSpaceState = SPACE_STATE_PHANTOM; } } @@ -1551,7 +1591,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction private static final class BatchInputUpdater implements Handler.Callback { private final Handler mHandler; private LatinIME mLatinIme; - private boolean mInBatchInput; // synchronized using "this". + private final Object mLock = new Object(); + private boolean mInBatchInput; // synchronized using {@link #mLock}. private BatchInputUpdater() { final HandlerThread handlerThread = new HandlerThread( @@ -1582,21 +1623,25 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } // Run in the UI thread. - public synchronized void onStartBatchInput(final LatinIME latinIme) { - mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); - mLatinIme = latinIme; - mInBatchInput = true; + public void onStartBatchInput(final LatinIME latinIme) { + synchronized (mLock) { + mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); + mLatinIme = latinIme; + mInBatchInput = true; + } } // Run in the Handler thread. - private synchronized void updateBatchInput(final InputPointers batchPointers) { - if (!mInBatchInput) { - // Batch input has ended or canceled while the message was being delivered. - return; + private void updateBatchInput(final InputPointers batchPointers) { + synchronized (mLock) { + if (!mInBatchInput) { + // Batch input has ended or canceled while the message was being delivered. + return; + } + final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); + mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( + suggestedWords, false /* dismissGestureFloatingPreviewText */); } - final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - suggestedWords, false /* dismissGestureFloatingPreviewText */); } // Run in the UI thread. @@ -1609,19 +1654,23 @@ public final class LatinIME extends InputMethodService implements KeyboardAction .sendToTarget(); } - public synchronized void onCancelBatchInput() { - mInBatchInput = false; - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); + public void onCancelBatchInput() { + synchronized (mLock) { + mInBatchInput = false; + mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( + SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); + } } // Run in the UI thread. - public synchronized SuggestedWords onEndBatchInput(final InputPointers batchPointers) { - mInBatchInput = false; - final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - suggestedWords, true /* dismissGestureFloatingPreviewText */); - return suggestedWords; + public SuggestedWords onEndBatchInput(final InputPointers batchPointers) { + synchronized (mLock) { + mInBatchInput = false; + final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers); + mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( + suggestedWords, true /* dismissGestureFloatingPreviewText */); + return suggestedWords; + } } // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to @@ -1717,6 +1766,12 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // during key repeat. mHandler.postUpdateShiftState(); + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can remove the character at the current cursor position. + resetEntireInputState(mLastSelectionStart); + // When we exit this if-clause, mWordComposer.isComposingWord() will return false. + } if (mWordComposer.isComposingWord()) { final int length = mWordComposer.size(); if (length > 0) { @@ -1727,7 +1782,9 @@ public final class LatinIME extends InputMethodService implements KeyboardAction ResearchLogger.getInstance().uncommitCurrentLogUnit( word, false /* dumpCurrentLogUnit */); } + final String rejectedSuggestion = mWordComposer.getTypedWord(); mWordComposer.reset(); + mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); } else { mWordComposer.deleteLast(); } @@ -1848,6 +1905,12 @@ public final class LatinIME extends InputMethodService implements KeyboardAction promotePhantomSpace(); } + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the character at the current cursor position. + resetEntireInputState(mLastSelectionStart); + isComposingWord = false; + } // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI // thread here. @@ -1902,6 +1965,38 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } } + private void handleRecapitalize() { + if (mLastSelectionStart == mLastSelectionEnd) return; // No selection + // If we have a recapitalize in progress, use it; otherwise, create a new one. + if (!mRecapitalizeStatus.isActive() + || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { + mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd, + mConnection.getSelectedText(0 /* flags, 0 for no styles */).toString(), + mSettings.getCurrentLocale(), mSettings.getWordSeparators()); + // We trim leading and trailing whitespace. + mRecapitalizeStatus.trim(); + // Trimming the object may have changed the length of the string, and we need to + // reposition the selection handles accordingly. As this result in an IPC call, + // only do it if it's actually necessary, in other words if the recapitalize status + // is not set at the same place as before. + if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { + mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); + mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); + mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); + } + } + mRecapitalizeStatus.rotate(); + final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; + mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); + mConnection.deleteSurroundingText(numCharsDeleted, 0); + mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); + mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); + mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); + mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); + // Match the keyboard to the new state. + mKeyboardSwitcher.updateShiftState(); + } + // Returns true if we did an autocorrection, false otherwise. private boolean handleSeparator(final int primaryCode, final int x, final int y, final int spaceState) { @@ -1909,7 +2004,11 @@ public final class LatinIME extends InputMethodService implements KeyboardAction ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord()); } boolean didAutoCorrect = false; - // Handle separator + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the separator at the current cursor position. + resetEntireInputState(mLastSelectionStart); + } if (mWordComposer.isComposingWord()) { if (mSettings.getCurrent().mCorrectionEnabled) { // TODO: maybe cache Strings in an <String> sparse array or something @@ -2321,6 +2420,73 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } /** + * Check if the cursor is touching a word. If so, restart suggestions on this word, else + * do nothing. + */ + private void restartSuggestionsOnWordTouchedByCursor() { + // If the cursor is not touching a word, or if there is a selection, return right away. + if (mLastSelectionStart != mLastSelectionEnd) return; + if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) return; + final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(), + 0 /* additionalPrecedingWordsCount */); + if (null == range) return; // Happens if we don't have an input connection at all + final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); + final String typedWord = range.mWord.toString(); + if (range.mWord instanceof SpannableString) { + final SpannableString spannableString = (SpannableString)range.mWord; + int i = 0; + for (Object object : spannableString.getSpans(0, spannableString.length(), + SuggestionSpan.class)) { + SuggestionSpan span = (SuggestionSpan)object; + for (String s : span.getSuggestions()) { + ++i; + if (!TextUtils.equals(s, typedWord)) { + suggestions.add(new SuggestedWordInfo(s, + SuggestionStripView.MAX_SUGGESTIONS - i, + SuggestedWordInfo.KIND_RESUMED, Dictionary.TYPE_RESUMED)); + } + } + } + } + mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard()); + mWordComposer.setCursorPositionWithinWord(range.mCharsBefore); + mConnection.setComposingRegion(mLastSelectionStart - range.mCharsBefore, + mLastSelectionEnd + range.mCharsAfter); + final SuggestedWords suggestedWords; + if (suggestions.isEmpty()) { + // We come here if there weren't any suggestion spans on this word. We will try to + // compute suggestions for it instead. + final SuggestedWords suggestedWordsIncludingTypedWord = + getSuggestedWords(Suggest.SESSION_TYPING); + if (suggestedWordsIncludingTypedWord.size() > 1) { + // We were able to compute new suggestions for this word. + // Remove the typed word, since we don't want to display it in this case. + // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false. + suggestedWords = + suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord(); + } else { + // No saved suggestions, and we were unable to compute any good one either. + // Rather than displaying an empty suggestion strip, we'll display the original + // word alone in the middle. + // Since there is only one word, willAutoCorrect is false. + suggestedWords = suggestedWordsIncludingTypedWord; + } + } else { + // We found suggestion spans in the word. We'll create the SuggestedWords out of + // them, and make willAutoCorrect false. + suggestedWords = new SuggestedWords(suggestions, + true /* typedWordValid */, false /* willAutoCorrect */, + false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, + false /* isPrediction */); + } + + // Note that it's very important here that suggestedWords.mWillAutoCorrect is false. + // We never want to auto-correct on a resumed suggestion. Please refer to the three + // places above where suggestedWords is affected. + showSuggestionStrip(suggestedWords, typedWord); + } + + /** * Check if the cursor is actually at the end of a word. If so, restart suggestions on this * word, else do nothing. */ @@ -2328,17 +2494,18 @@ public final class LatinIME extends InputMethodService implements KeyboardAction final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent()); if (null != word) { - restartSuggestionsOnWordBeforeCursor(word); + final String wordString = word.toString(); + restartSuggestionsOnWordBeforeCursor(wordString); // TODO: Handle the case where the user manually moves the cursor and then backs up over // a separator. In that case, the current log unit should not be uncommitted. if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().uncommitCurrentLogUnit(word.toString(), + ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString, true /* dumpCurrentLogUnit */); } } } - private void restartSuggestionsOnWordBeforeCursor(final CharSequence word) { + private void restartSuggestionsOnWordBeforeCursor(final String word) { mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); final int length = word.length(); mConnection.deleteSurroundingText(length, 0); @@ -2392,7 +2559,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // This essentially inserts a space, and that's it. public void promotePhantomSpace() { - if (mSettings.getCurrent().shouldInsertSpacesAutomatically()) { + if (mSettings.getCurrent().shouldInsertSpacesAutomatically() + && !mConnection.textBeforeCursorLooksLikeURL()) { sendKeyCodePoint(Constants.CODE_SPACE); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_promotePhantomSpace(); diff --git a/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java b/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java new file mode 100644 index 000000000..e6dc6db8f --- /dev/null +++ b/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2013 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; + +/** + * Helper class to get the metadata URI. + */ +public class MetadataFileUriGetter { + public static String getMetadataUri(Context context) { + return context.getString(R.string.dictionary_pack_metadata_uri); + } +} diff --git a/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java b/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java new file mode 100644 index 000000000..8a704ab42 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin; + +import com.android.inputmethod.latin.StringUtils; + +import java.util.Locale; + +/** + * The status of the current recapitalize process. + */ +public class RecapitalizeStatus { + public static final int NOT_A_RECAPITALIZE_MODE = -1; + public static final int CAPS_MODE_ORIGINAL_MIXED_CASE = 0; + public static final int CAPS_MODE_ALL_LOWER = 1; + public static final int CAPS_MODE_FIRST_WORD_UPPER = 2; + public static final int CAPS_MODE_ALL_UPPER = 3; + // When adding a new mode, don't forget to update the CAPS_MODE_LAST constant. + public static final int CAPS_MODE_LAST = CAPS_MODE_ALL_UPPER; + + private static final int[] ROTATION_STYLE = { + CAPS_MODE_ORIGINAL_MIXED_CASE, + CAPS_MODE_ALL_LOWER, + CAPS_MODE_FIRST_WORD_UPPER, + CAPS_MODE_ALL_UPPER + }; + + private static final int getStringMode(final String string, final String separators) { + if (StringUtils.isIdenticalAfterUpcase(string)) { + return CAPS_MODE_ALL_UPPER; + } else if (StringUtils.isIdenticalAfterDowncase(string)) { + return CAPS_MODE_ALL_LOWER; + } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, separators)) { + return CAPS_MODE_FIRST_WORD_UPPER; + } else { + return CAPS_MODE_ORIGINAL_MIXED_CASE; + } + } + + /** + * We store the location of the cursor and the string that was there before the recapitalize + * action was done, and the location of the cursor and the string that was there after. + */ + private int mCursorStartBefore; + private String mStringBefore; + private int mCursorStartAfter; + private int mCursorEndAfter; + private int mRotationStyleCurrentIndex; + private boolean mSkipOriginalMixedCaseMode; + private Locale mLocale; + private String mSeparators; + private String mStringAfter; + private boolean mIsActive; + + public RecapitalizeStatus() { + // By default, initialize with dummy values that won't match any real recapitalize. + initialize(-1, -1, "", Locale.getDefault(), ""); + deactivate(); + } + + public void initialize(final int cursorStart, final int cursorEnd, final String string, + final Locale locale, final String separators) { + mCursorStartBefore = cursorStart; + mStringBefore = string; + mCursorStartAfter = cursorStart; + mCursorEndAfter = cursorEnd; + mStringAfter = string; + final int initialMode = getStringMode(mStringBefore, separators); + mLocale = locale; + mSeparators = separators; + if (CAPS_MODE_ORIGINAL_MIXED_CASE == initialMode) { + mRotationStyleCurrentIndex = 0; + mSkipOriginalMixedCaseMode = false; + } else { + // Find the current mode in the array. + int currentMode; + for (currentMode = ROTATION_STYLE.length - 1; currentMode > 0; --currentMode) { + if (ROTATION_STYLE[currentMode] == initialMode) { + break; + } + } + mRotationStyleCurrentIndex = currentMode; + mSkipOriginalMixedCaseMode = true; + } + mIsActive = true; + } + + public void deactivate() { + mIsActive = false; + } + + public boolean isActive() { + return mIsActive; + } + + public boolean isSetAt(final int cursorStart, final int cursorEnd) { + return cursorStart == mCursorStartAfter && cursorEnd == mCursorEndAfter; + } + + /** + * Rotate through the different possible capitalization modes. + */ + public void rotate() { + final String oldResult = mStringAfter; + int count = 0; // Protection against infinite loop. + do { + mRotationStyleCurrentIndex = (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length; + if (CAPS_MODE_ORIGINAL_MIXED_CASE == ROTATION_STYLE[mRotationStyleCurrentIndex] + && mSkipOriginalMixedCaseMode) { + mRotationStyleCurrentIndex = + (mRotationStyleCurrentIndex + 1) % ROTATION_STYLE.length; + } + ++count; + switch (ROTATION_STYLE[mRotationStyleCurrentIndex]) { + case CAPS_MODE_ORIGINAL_MIXED_CASE: + mStringAfter = mStringBefore; + break; + case CAPS_MODE_ALL_LOWER: + mStringAfter = mStringBefore.toLowerCase(mLocale); + break; + case CAPS_MODE_FIRST_WORD_UPPER: + mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSeparators, + mLocale); + break; + case CAPS_MODE_ALL_UPPER: + mStringAfter = mStringBefore.toUpperCase(mLocale); + break; + default: + mStringAfter = mStringBefore; + } + } while (mStringAfter.equals(oldResult) && count < ROTATION_STYLE.length + 1); + mCursorEndAfter = mCursorStartAfter + mStringAfter.length(); + } + + /** + * Remove leading/trailing whitespace from the considered string. + */ + public void trim() { + final int len = mStringBefore.length(); + int nonWhitespaceStart = 0; + for (; nonWhitespaceStart < len; + nonWhitespaceStart = mStringBefore.offsetByCodePoints(nonWhitespaceStart, 1)) { + final int codePoint = mStringBefore.codePointAt(nonWhitespaceStart); + if (!Character.isWhitespace(codePoint)) break; + } + int nonWhitespaceEnd = len; + for (; nonWhitespaceEnd > 0; + nonWhitespaceEnd = mStringBefore.offsetByCodePoints(nonWhitespaceEnd, -1)) { + final int codePoint = mStringBefore.codePointBefore(nonWhitespaceEnd); + if (!Character.isWhitespace(codePoint)) break; + } + if (0 != nonWhitespaceStart || len != nonWhitespaceEnd) { + mCursorEndAfter = mCursorStartBefore + nonWhitespaceEnd; + mCursorStartBefore = mCursorStartAfter = mCursorStartBefore + nonWhitespaceStart; + mStringAfter = mStringBefore = + mStringBefore.substring(nonWhitespaceStart, nonWhitespaceEnd); + } + } + + public String getRecapitalizedString() { + return mStringAfter; + } + + public int getNewCursorStart() { + return mCursorStartAfter; + } + + public int getNewCursorEnd() { + return mCursorEndAfter; + } + + public int getCurrentMode() { + return ROTATION_STYLE[mRotationStyleCurrentIndex]; + } +} diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 16744d1f0..8ed7ab264 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin; import android.inputmethodservice.InputMethodService; +import android.text.SpannableString; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; @@ -181,6 +182,11 @@ public final class RichInputConnection { } } + public CharSequence getSelectedText(final int flags) { + if (null == mIC) return null; + return mIC.getSelectedText(flags); + } + /** * Gets the caps modes we should be in after this specific string. * @@ -392,7 +398,9 @@ public final class RichInputConnection { public void commitCompletion(final CompletionInfo completionInfo) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - final CharSequence text = completionInfo.getText(); + CharSequence text = completionInfo.getText(); + // text should never be null, but just in case, it's better to insert nothing than to crash + if (null == text) text = ""; mCommittedTextBeforeComposingText.append(text); mCurrentCursorPosition += text.length() - mComposingText.length(); mComposingText.setLength(0); @@ -442,9 +450,9 @@ public final class RichInputConnection { public final int mCharsAfter; /** The actual characters that make up a word */ - public final String mWord; + public final CharSequence mWord; - public Range(int charsBefore, int charsAfter, String word) { + public Range(int charsBefore, int charsAfter, CharSequence word) { if (charsBefore < 0 || charsAfter < 0) { throw new IndexOutOfBoundsException(); } @@ -498,7 +506,7 @@ public final class RichInputConnection { * separator. For example, if the field contains "he|llo world", where | * represents the cursor, then "hello " will be returned. */ - public String getWordAtCursor(String separators) { + public CharSequence getWordAtCursor(String separators) { // getWordRangeAtCursor returns null if the connection is null Range r = getWordRangeAtCursor(separators, 0); return (r == null) ? null : r.mWord; @@ -517,8 +525,10 @@ public final class RichInputConnection { if (mIC == null || sep == null) { return null; } - final CharSequence before = mIC.getTextBeforeCursor(1000, 0); - final CharSequence after = mIC.getTextAfterCursor(1000, 0); + final CharSequence before = mIC.getTextBeforeCursor(1000, + InputConnection.GET_TEXT_WITH_STYLES); + final CharSequence after = mIC.getTextAfterCursor(1000, + InputConnection.GET_TEXT_WITH_STYLES); if (before == null || after == null) { return null; } @@ -560,8 +570,9 @@ public final class RichInputConnection { } } - final String word = before.toString().substring(startIndexInBefore, before.length()) - + after.toString().substring(0, endIndexInAfter); + final SpannableString word = new SpannableString(TextUtils.concat( + before.subSequence(startIndexInBefore, before.length()), + after.subSequence(0, endIndexInAfter))); return new Range(before.length() - startIndexInBefore, endIndexInAfter, word); } @@ -709,4 +720,15 @@ public final class RichInputConnection { // position and the expected position, then it must be a belated update. return (newSelStart - oldSelStart) * (mCurrentCursorPosition - newSelStart) >= 0; } + + /** + * Looks at the text just before the cursor to find out if it looks like a URL. + * + * The weakest point here is, if we don't have enough text bufferized, we may fail to realize + * we are in URL situation, but other places in this class have the same limitation and it + * does not matter too much in the practice. + */ + public boolean textBeforeCursorLooksLikeURL() { + return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); + } } diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java index 8fbe843cf..72e08700a 100644 --- a/java/src/com/android/inputmethod/latin/Settings.java +++ b/java/src/com/android/inputmethod/latin/Settings.java @@ -134,6 +134,14 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return mSettingsValues.mIsInternal; } + public String getWordSeparators() { + return mSettingsValues.mWordSeparators; + } + + public Locale getCurrentLocale() { + return mCurrentLocale; + } + // Accessed from the settings interface, hence public public static boolean readKeypressSoundEnabled(final SharedPreferences prefs, final Resources res) { diff --git a/java/src/com/android/inputmethod/latin/SettingsActivity.java b/java/src/com/android/inputmethod/latin/SettingsActivity.java index 99b572e06..37ac2e35c 100644 --- a/java/src/com/android/inputmethod/latin/SettingsActivity.java +++ b/java/src/com/android/inputmethod/latin/SettingsActivity.java @@ -25,7 +25,10 @@ public final class SettingsActivity extends PreferenceActivity { @Override public Intent getIntent() { final Intent intent = super.getIntent(); - intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT); + final String fragment = intent.getStringExtra(EXTRA_SHOW_FRAGMENT); + if (fragment == null) { + intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT); + } intent.putExtra(EXTRA_NO_HEADERS, true); return intent; } diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java index 5405a5eb7..a96c997c8 100644 --- a/java/src/com/android/inputmethod/latin/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java @@ -16,7 +16,6 @@ package com.android.inputmethod.latin; -import android.app.Activity; import android.app.backup.BackupManager; import android.content.Context; import android.content.Intent; @@ -115,12 +114,8 @@ public final class SettingsFragment extends InputMethodSettingsFragment if (FeedbackUtils.isFeedbackFormSupported()) { feedbackSettings.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override - public boolean onPreferenceClick(Preference arg0) { - final Activity activity = getActivity(); - FeedbackUtils.showFeedbackForm(activity); - if (!activity.isFinishing()) { - activity.finish(); - } + public boolean onPreferenceClick(final Preference pref) { + FeedbackUtils.showFeedbackForm(getActivity()); return true; } }); @@ -232,10 +227,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment } else if (key.equals(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY)) { setPreferenceEnabled(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, Settings.readShowsLanguageSwitchKey(prefs)); - } else if (key.equals(Settings.PREF_GESTURE_INPUT)) { - final boolean gestureInputEnabled = Settings.readGestureInputEnabled(prefs, res); - setPreferenceEnabled(Settings.PREF_GESTURE_PREVIEW_TRAIL, gestureInputEnabled); - setPreferenceEnabled(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, gestureInputEnabled); } else if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) { LauncherIconVisibilityManager.updateSetupWizardIconVisibility(getActivity()); } diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java index 59ad28fc9..d5ee58a63 100644 --- a/java/src/com/android/inputmethod/latin/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/StringUtils.java @@ -106,10 +106,19 @@ public final class StringUtils { } } - public static String toTitleCase(final String s, final Locale locale) { + public static String capitalizeFirstCodePoint(final String s, final Locale locale) { if (s.length() <= 1) { - // TODO: is this really correct? Shouldn't this be s.toUpperCase()? - return s; + return s.toUpperCase(locale); + } + // Please refer to the comment below in + // {@link #capitalizeFirstAndDowncaseRest(String,Locale)} as this has the same shortcomings + final int cutoff = s.offsetByCodePoints(0, 1); + return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff); + } + + public static String capitalizeFirstAndDowncaseRest(final String s, final Locale locale) { + if (s.length() <= 1) { + return s.toUpperCase(locale); } // TODO: fix the bugs below // - This does not work for Greek, because it returns upper case instead of title case. @@ -213,4 +222,129 @@ public final class StringUtils { if (1 == capsCount) return CAPITALIZE_FIRST; return (letterCount == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE); } + + public static boolean isIdenticalAfterUpcase(final String text) { + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + if (Character.isLetter(codePoint) && !Character.isUpperCase(codePoint)) { + return false; + } + } + return true; + } + + public static boolean isIdenticalAfterDowncase(final String text) { + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + if (Character.isLetter(codePoint) && !Character.isLowerCase(codePoint)) { + return false; + } + } + return true; + } + + public static boolean isIdenticalAfterCapitalizeEachWord(final String text, + final String separators) { + boolean needCapsNext = true; + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final int codePoint = text.codePointAt(i); + if (Character.isLetter(codePoint)) { + if ((needCapsNext && !Character.isUpperCase(codePoint)) + || (!needCapsNext && !Character.isLowerCase(codePoint))) { + return false; + } + } + // We need a capital letter next if this is a separator. + needCapsNext = (-1 != separators.indexOf(codePoint)); + } + return true; + } + + // TODO: like capitalizeFirst*, this does not work perfectly for Dutch because of the IJ digraph + // which should be capitalized together in *some* cases. + public static String capitalizeEachWord(final String text, final String separators, + final Locale locale) { + final StringBuilder builder = new StringBuilder(); + boolean needCapsNext = true; + final int len = text.length(); + for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { + final String nextChar = text.substring(i, text.offsetByCodePoints(i, 1)); + if (needCapsNext) { + builder.append(nextChar.toUpperCase(locale)); + } else { + builder.append(nextChar.toLowerCase(locale)); + } + // We need a capital letter next if this is a separator. + needCapsNext = (-1 != separators.indexOf(nextChar.codePointAt(0))); + } + return builder.toString(); + } + + /** + * Approximates whether the text before the cursor looks like a URL. + * + * This is not foolproof, but it should work well in the practice. + * Essentially it walks backward from the cursor until it finds something that's not a letter, + * digit, or common URL symbol like underscore. If it hasn't found a period yet, then it + * does not look like a URL. + * If the text: + * - starts with www and contains a period + * - starts with a slash preceded by either a slash, whitespace, or start-of-string + * Then it looks like a URL and we return true. Otherwise, we return false. + * + * Note: this method is called quite often, and should be fast. + * + * TODO: This will return that "abc./def" and ".abc/def" look like URLs to keep down the + * code complexity, but ideally it should not. It's acceptable for now. + */ + public static boolean lastPartLooksLikeURL(final CharSequence text) { + int i = text.length(); + if (0 == i) return false; + int wCount = 0; + int slashCount = 0; + boolean hasSlash = false; + boolean hasPeriod = false; + int codePoint = 0; + while (i > 0) { + codePoint = Character.codePointBefore(text, i); + if (codePoint < Constants.CODE_PERIOD || codePoint > 'z') { + // Handwavy heuristic to see if that's a URL character. Anything between period + // and z. This includes all lower- and upper-case ascii letters, period, + // underscore, arrobase, question mark, equal sign. It excludes spaces, exclamation + // marks, double quotes... + // Anything that's not a URL-like character causes us to break from here and + // evaluate normally. + break; + } + if (Constants.CODE_PERIOD == codePoint) { + hasPeriod = true; + } + if (Constants.CODE_SLASH == codePoint) { + hasSlash = true; + if (2 == ++slashCount) { + return true; + } + } else { + slashCount = 0; + } + if ('w' == codePoint) { + ++wCount; + } else { + wCount = 0; + } + i = Character.offsetByCodePoints(text, i, -1); + } + // End of the text run. + // If it starts with www and includes a period, then it looks like a URL. + if (wCount >= 3 && hasPeriod) return true; + // If it starts with a slash, and the code point before is whitespace, it looks like an URL. + if (1 == slashCount && (0 == i || Character.isWhitespace(codePoint))) return true; + // If it has both a period and a slash, it looks like an URL. + if (hasPeriod && hasSlash) return true; + // Otherwise, it doesn't look like an URL. + return false; + } } diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java index 5e28cc2d0..4d88ecc0c 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java +++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java @@ -183,7 +183,7 @@ public final class SubtypeLocale { final Locale locale = LocaleUtils.constructLocaleFromString(localeString); displayName = locale.getDisplayName(displayLocale); } - return StringUtils.toTitleCase(displayName, displayLocale); + return StringUtils.capitalizeFirstCodePoint(displayName, displayLocale); } // InputMethodSubtype's display name in its locale. @@ -243,7 +243,7 @@ public final class SubtypeLocale { } } }; - return StringUtils.toTitleCase( + return StringUtils.capitalizeFirstCodePoint( getSubtypeName.runInLocale(sResources, displayLocale), displayLocale); } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 975664dca..671d7146b 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -47,6 +47,9 @@ public final class Suggest { // TODO: rename this to CORRECTION_ON public static final int CORRECTION_FULL = 1; + // Close to -2**31 + private static final int SUPPRESS_SUGGEST_THRESHOLD = -2000000000; + public interface SuggestInitializationListener { public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); } @@ -334,7 +337,21 @@ public final class Suggest { } } + if (suggestionsContainer.size() > 1 && TextUtils.equals(suggestionsContainer.get(0).mWord, + wordComposer.getRejectedBatchModeSuggestion())) { + final SuggestedWordInfo rejected = suggestionsContainer.remove(0); + suggestionsContainer.add(1, rejected); + } SuggestedWordInfo.removeDups(suggestionsContainer); + + // For some reason some suggestions with MIN_VALUE are making their way here. + // TODO: Find a more robust way to detect distractors. + for (int i = suggestionsContainer.size() - 1; i >= 0; --i) { + if (suggestionsContainer.get(i).mScore < SUPPRESS_SUGGEST_THRESHOLD) { + suggestionsContainer.remove(i); + } + } + // In the batch input mode, the most relevant suggested word should act as a "typed word" // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false). return new SuggestedWords(suggestionsContainer, @@ -394,7 +411,7 @@ public final class Suggest { if (isAllUpperCase) { sb.append(wordInfo.mWord.toUpperCase(locale)); } else if (isFirstCharCapitalized) { - sb.append(StringUtils.toTitleCase(wordInfo.mWord, locale)); + sb.append(StringUtils.capitalizeFirstCodePoint(wordInfo.mWord, locale)); } else { sb.append(wordInfo.mWord); } diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 3d6fe2d22..616e1911b 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -131,6 +131,7 @@ public final class SuggestedWords { public static final int KIND_APP_DEFINED = 6; // Suggested by the application public static final int KIND_SHORTCUT = 7; // A shortcut public static final int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input) + public static final int KIND_RESUMED = 9; // A resumed suggestion (comes from a span) public final String mWord; public final int mScore; public final int mKind; // one of the KIND_* constants above @@ -194,4 +195,21 @@ public final class SuggestedWords { } } } + + // SuggestedWords is an immutable object, as much as possible. We must not just remove + // words from the member ArrayList as some other parties may expect the object to never change. + public SuggestedWords getSuggestedWordsExcludingTypedWord() { + final ArrayList<SuggestedWordInfo> newSuggestions = CollectionUtils.newArrayList(); + for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { + final SuggestedWordInfo info = mSuggestedWordInfoList.get(i); + if (SuggestedWordInfo.KIND_TYPED != info.mKind) { + newSuggestions.add(info); + } + } + // We should never autocorrect, so we say the typed word is valid. Also, in this case, + // no auto-correction should take place hence willAutoCorrect = false. + return new SuggestedWords(newSuggestions, true /* typedWordValid */, + false /* willAutoCorrect */, mIsPunctuationSuggestions, mIsObsoleteSuggestions, + mIsPrediction); + } } diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java index 0d5bde623..90f92972a 100644 --- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java @@ -20,7 +20,6 @@ import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; -import android.content.Intent; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; @@ -28,7 +27,10 @@ import android.os.Build; import android.provider.UserDictionary.Words; import android.text.TextUtils; +import com.android.inputmethod.compat.UserDictionaryCompatUtils; + import java.util.Arrays; +import java.util.Locale; /** * An expandable dictionary that stores the words in the user dictionary provider into a binary @@ -61,10 +63,6 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { private static final String NAME = "userunigram"; - // This is not exported by the framework so we pretty much have to write it here verbatim - private static final String ACTION_USER_DICTIONARY_INSERT = - "com.android.settings.USER_DICTIONARY_INSERT"; - private ContentObserver mObserver; final private String mLocale; final private boolean mAlsoUseMoreRestrictiveLocales; @@ -211,23 +209,19 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { /** * Adds a word to the user dictionary and makes it persistent. * - * This will call upon the system interface to do the actual work through the intent readied by - * the system to this effect. - * * @param word the word to add. If the word is capitalized, then the dictionary will * recognize it as a capitalized word when searched. */ public synchronized void addWordToUserDictionary(final String word) { - // TODO: do something for the UI. With the following, any sufficiently long word will - // look like it will go to the user dictionary but it won't. - // Safeguard against adding long words. Can cause stack overflow. - if (word.length() >= MAX_WORD_LENGTH) return; - - Intent intent = new Intent(ACTION_USER_DICTIONARY_INSERT); - intent.putExtra(Words.WORD, word); - intent.putExtra(Words.LOCALE, mLocale); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(intent); + // Update the user dictionary provider + final Locale locale; + if (USER_DICTIONARY_ALL_LANGUAGES == mLocale) { + locale = null; + } else { + locale = LocaleUtils.constructLocaleFromString(mLocale); + } + UserDictionaryCompatUtils.addWord(mContext, word, + HISTORICAL_DEFAULT_USER_DICTIONARY_FREQUENCY, null, locale); } private int scaleFrequencyFromDefaultToLatinIme(final int defaultFrequency) { @@ -258,10 +252,10 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency); // Safeguard against adding really long words. if (word.length() < MAX_WORD_LENGTH) { - super.addWord(word, null, adjustedFrequency); + super.addWord(word, null, adjustedFrequency, false /* isNotAWord */); } if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) { - super.addWord(shortcut, word, adjustedFrequency); + super.addWord(shortcut, word, adjustedFrequency, true /* isNotAWord */); } cursor.moveToNext(); } diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index f7cb4346a..51bd901fb 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -27,6 +27,7 @@ import java.util.Arrays; */ public final class WordComposer { private static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH; + private static final boolean DBG = LatinImeLogger.sDBG; public static final int CAPS_MODE_OFF = 0; // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits @@ -42,6 +43,13 @@ public final class WordComposer { private String mAutoCorrection; private boolean mIsResumed; private boolean mIsBatchMode; + // A memory of the last rejected batch mode suggestion, if any. This goes like this: the user + // gestures a word, is displeased with the results and hits backspace, then gestures again. + // At the very least we should avoid re-suggesting the same thing, and to do that we memorize + // the rejected suggestion in this variable. + // TODO: this should be done in a comprehensive way by the User History feature instead of + // as an ad-hockery here. + private String mRejectedBatchModeSuggestion; // Cache these values for performance private int mCapsCount; @@ -49,6 +57,7 @@ public final class WordComposer { private int mCapitalizedMode; private int mTrailingSingleQuotesCount; private int mCodePointSize; + private int mCursorPositionWithinWord; /** * Whether the user chose to capitalize the first char of the word. @@ -62,6 +71,8 @@ public final class WordComposer { mTrailingSingleQuotesCount = 0; mIsResumed = false; mIsBatchMode = false; + mCursorPositionWithinWord = 0; + mRejectedBatchModeSuggestion = null; refreshSize(); } @@ -76,6 +87,8 @@ public final class WordComposer { mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; mIsResumed = source.mIsResumed; mIsBatchMode = source.mIsBatchMode; + mCursorPositionWithinWord = source.mCursorPositionWithinWord; + mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion; refreshSize(); } @@ -91,6 +104,8 @@ public final class WordComposer { mTrailingSingleQuotesCount = 0; mIsResumed = false; mIsBatchMode = false; + mCursorPositionWithinWord = 0; + mRejectedBatchModeSuggestion = null; refreshSize(); } @@ -118,6 +133,13 @@ public final class WordComposer { return mPrimaryKeyCodes[index]; } + public int getCodeBeforeCursor() { + if (mCursorPositionWithinWord < 1 || mCursorPositionWithinWord > mPrimaryKeyCodes.length) { + return Constants.NOT_A_CODE; + } + return mPrimaryKeyCodes[mCursorPositionWithinWord - 1]; + } + public InputPointers getInputPointers() { return mInputPointers; } @@ -135,6 +157,7 @@ public final class WordComposer { final int newIndex = size(); mTypedWord.appendCodePoint(primaryCode); refreshSize(); + mCursorPositionWithinWord = mCodePointSize; if (newIndex < MAX_WORD_LENGTH) { mPrimaryKeyCodes[newIndex] = primaryCode >= Constants.CODE_SPACE ? Character.toLowerCase(primaryCode) : primaryCode; @@ -158,6 +181,18 @@ public final class WordComposer { mAutoCorrection = null; } + public void setCursorPositionWithinWord(final int posWithinWord) { + mCursorPositionWithinWord = posWithinWord; + } + + public boolean isCursorFrontOrMiddleOfComposingWord() { + if (DBG && mCursorPositionWithinWord > mCodePointSize) { + throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord + + "in a word of size " + mCodePointSize); + } + return mCursorPositionWithinWord != mCodePointSize; + } + public void setBatchInputPointers(final InputPointers batchPointers) { mInputPointers.set(batchPointers); mIsBatchMode = true; @@ -242,6 +277,7 @@ public final class WordComposer { ++mTrailingSingleQuotesCount; } } + mCursorPositionWithinWord = mCodePointSize; mAutoCorrection = null; } @@ -368,7 +404,9 @@ public final class WordComposer { mCapitalizedMode = CAPS_MODE_OFF; refreshSize(); mAutoCorrection = null; + mCursorPositionWithinWord = 0; mIsResumed = false; + mRejectedBatchModeSuggestion = null; return lastComposedWord; } @@ -380,10 +418,20 @@ public final class WordComposer { refreshSize(); mCapitalizedMode = lastComposedWord.mCapitalizedMode; mAutoCorrection = null; // This will be filled by the next call to updateSuggestion. + mCursorPositionWithinWord = mCodePointSize; + mRejectedBatchModeSuggestion = null; mIsResumed = true; } public boolean isBatchMode() { return mIsBatchMode; } + + public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) { + mRejectedBatchModeSuggestion = rejectedSuggestion; + } + + public String getRejectedBatchModeSuggestion() { + return mRejectedBatchModeSuggestion; + } } diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java index 58ec1e83f..467f6a053 100644 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java +++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java @@ -1467,8 +1467,8 @@ public final class BinaryDictInputOutput { if (null == last) continue; builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); buffer.position(last.mChildrenAddress + headerSize); - groupOffset = last.mChildrenAddress + 1; - i = buffer.readUnsignedByte(); + i = readCharGroupCount(buffer); + groupOffset = last.mChildrenAddress + getGroupCountSize(i); last = null; continue; } @@ -1477,8 +1477,8 @@ public final class BinaryDictInputOutput { if (0 == i && hasChildrenAddress(last.mChildrenAddress)) { builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); buffer.position(last.mChildrenAddress + headerSize); - groupOffset = last.mChildrenAddress + 1; - i = buffer.readUnsignedByte(); + i = readCharGroupCount(buffer); + groupOffset = last.mChildrenAddress + getGroupCountSize(i); last = null; continue; } diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java index e7c7e2b8a..17d281518 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java @@ -647,7 +647,7 @@ public final class FusionDictionary implements Iterable<Word> { if (index < codePoints.length) return null; if (!currentGroup.isTerminal()) return null; - if (DBG && !codePoints.equals(checker.toString())) return null; + if (DBG && !string.equals(checker.toString())) return null; return currentGroup; } @@ -853,16 +853,19 @@ public final class FusionDictionary implements Iterable<Word> { if (currentPos.pos.hasNext()) { final CharGroup currentGroup = currentPos.pos.next(); currentPos.length = mCurrentString.length(); - for (int i : currentGroup.mChars) + for (int i : currentGroup.mChars) { mCurrentString.append(Character.toChars(i)); + } if (null != currentGroup.mChildren) { currentPos = new Position(currentGroup.mChildren.mData); + currentPos.length = mCurrentString.length(); mPositions.addLast(currentPos); } - if (currentGroup.mFrequency >= 0) + if (currentGroup.mFrequency >= 0) { return new Word(mCurrentString.toString(), currentGroup.mFrequency, currentGroup.mShortcutTargets, currentGroup.mBigrams, currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry); + } } else { mPositions.removeLast(); currentPos = mPositions.getLast(); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 8d3b062ff..2d0a89bb3 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -330,7 +330,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService } else if (StringUtils.CAPITALIZE_FIRST == capitalizeType) { for (int i = 0; i < mSuggestions.size(); ++i) { // Likewise - mSuggestions.set(i, StringUtils.toTitleCase( + mSuggestions.set(i, StringUtils.capitalizeFirstCodePoint( mSuggestions.get(i).toString(), locale)); } } @@ -403,11 +403,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService public DictAndProximity createDictAndProximity(final Locale locale) { final int script = getScriptFromLocale(locale); - final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo( - SpellCheckerProximityInfo.getProximityForScript(script), - SpellCheckerProximityInfo.ROW_SIZE, - SpellCheckerProximityInfo.PROXIMITY_GRID_WIDTH, - SpellCheckerProximityInfo.PROXIMITY_GRID_HEIGHT); + final ProximityInfo proximityInfo = new SpellCheckerProximityInfo(script); final DictionaryCollection dictionaryCollection = DictionaryFactory.createMainDictionaryFromManager(this, locale, true /* useFullEditDistance */); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index b15063235..da8657201 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -189,10 +189,12 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { int letterCount = 0; for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) { final int codePoint = text.codePointAt(i); - // Any word containing a '@' is probably an e-mail address - // Any word containing a '/' is probably either an ad-hoc combination of two + // Any word containing a COMMERCIAL_AT is probably an e-mail address + // Any word containing a SLASH is probably either an ad-hoc combination of two // words or a URI - in either case we don't want to spell check that - if ('@' == codePoint || '/' == codePoint) return true; + if (Constants.CODE_COMMERCIAL_AT == codePoint || Constants.CODE_SLASH == codePoint) { + return true; + } if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount; } // Guestimate heuristic: perform spell checking if at least 3/4 of the characters @@ -226,7 +228,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { // If the lower case version is not in the dictionary, it's still possible // that we have an all-caps version of a word that needs to be capitalized // according to the dictionary. E.g. "GERMANS" only exists in the dictionary as "Germans". - return dict.isValidWord(StringUtils.toTitleCase(lowerCaseText, mLocale)); + return dict.isValidWord(StringUtils.capitalizeFirstAndDowncaseRest(lowerCaseText, mLocale)); } // Note : this must be reentrant diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java index 49dca21e6..0c480eaba 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java @@ -16,53 +16,40 @@ package com.android.inputmethod.latin.spellcheck; -import com.android.inputmethod.annotations.UsedForTesting; +import android.util.SparseIntArray; + import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.CollectionUtils; import com.android.inputmethod.latin.Constants; -import java.util.TreeMap; +public final class SpellCheckerProximityInfo extends ProximityInfo { + public SpellCheckerProximityInfo(final int script) { + super(getProximityForScript(script), PROXIMITY_GRID_WIDTH, PROXIMITY_GRID_HEIGHT); + } -public final class SpellCheckerProximityInfo { - @UsedForTesting - final public static int NUL = Constants.NOT_A_CODE; + private static final int NUL = Constants.NOT_A_CODE; // This must be the same as MAX_PROXIMITY_CHARS_SIZE else it will not work inside // native code - this value is passed at creation of the binary object and reused // as the size of the passed array afterwards so they can't be different. - final public static int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE; + private static final int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE; // The number of keys in a row of the grid used by the spell checker. - final public static int PROXIMITY_GRID_WIDTH = 11; + private static final int PROXIMITY_GRID_WIDTH = 11; // The number of rows in the grid used by the spell checker. - final public static int PROXIMITY_GRID_HEIGHT = 3; + private static final int PROXIMITY_GRID_HEIGHT = 3; - final private static int NOT_AN_INDEX = -1; - final public static int NOT_A_COORDINATE_PAIR = -1; + private static final int NOT_AN_INDEX = -1; + public static final int NOT_A_COORDINATE_PAIR = -1; // Helper methods - final protected static void buildProximityIndices(final int[] proximity, - final TreeMap<Integer, Integer> indices) { - for (int i = 0; i < proximity.length; i += ROW_SIZE) { - if (NUL != proximity[i]) indices.put(proximity[i], i / ROW_SIZE); + static void buildProximityIndices(final int[] proximity, final int rowSize, + final SparseIntArray indices) { + for (int i = 0; i < proximity.length; i += rowSize) { + if (NUL != proximity[i]) indices.put(proximity[i], i / rowSize); } } - final protected static int computeIndex(final int characterCode, - final TreeMap<Integer, Integer> indices) { - final Integer result = indices.get(characterCode); - if (null == result) return NOT_AN_INDEX; - return result; - } private static final class Latin { - // This is a map from the code point to the index in the PROXIMITY array. - // At the time the native code to read the binary dictionary needs the proximity info be - // passed as a flat array spaced by MAX_PROXIMITY_CHARS_SIZE columns, one for each input - // character. - // Since we need to build such an array, we want to be able to search in our big proximity - // data quickly by character, and a map is probably the best way to do this. - final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap(); - // The proximity here is the union of // - the proximity for a QWERTY keyboard. // - the proximity for an AZERTY keyboard. @@ -79,7 +66,7 @@ public final class SpellCheckerProximityInfo { a s d f g h j k l z x c v b n m */ - final static int[] PROXIMITY = { + static final int[] PROXIMITY = { // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter, // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's. // The number of rows must be exactly PROXIMITY_GRID_HEIGHT. @@ -121,16 +108,21 @@ public final class SpellCheckerProximityInfo { NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, }; + + // This is a mapping array from the code point to the index in the PROXIMITY array. + // When we check the spelling of a word, we need to pass (x,y) coordinates to the native + // code for each letter of the word. These are most easily computed from the index in the + // PROXIMITY array. Since we'll need to do that very often, the index lookup from the code + // point needs to be as fast as possible, and a map is probably the best way to do this. + // To avoid unnecessary boxing conversion to Integer, here we use SparseIntArray. + static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE); + static { - buildProximityIndices(PROXIMITY, INDICES); - } - static int getIndexOf(int characterCode) { - return computeIndex(characterCode, INDICES); + buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES); } } private static final class Cyrillic { - final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap(); // TODO: The following table is solely based on the keyboard layout. Consult with Russian // speakers on commonly misspelled words/letters. /* @@ -207,7 +199,7 @@ public final class SpellCheckerProximityInfo { private static final int CY_SOFT_SIGN = '\u044C'; // ь private static final int CY_BE = '\u0431'; // б private static final int CY_YU = '\u044E'; // ю - final static int[] PROXIMITY = { + static final int[] PROXIMITY = { // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter, // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's. // The number of rows must be exactly PROXIMITY_GRID_HEIGHT. @@ -280,16 +272,15 @@ public final class SpellCheckerProximityInfo { NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, }; + + static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE); + static { - buildProximityIndices(PROXIMITY, INDICES); - } - static int getIndexOf(int characterCode) { - return computeIndex(characterCode, INDICES); + buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES); } } private static final class Greek { - final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap(); // TODO: The following table is solely based on the keyboard layout. Consult with Greek // speakers on commonly misspelled words/letters. /* @@ -354,7 +345,7 @@ public final class SpellCheckerProximityInfo { private static final int GR_BETA = '\u03B2'; // β private static final int GR_NU = '\u03BD'; // ν private static final int GR_MU = '\u03BC'; // μ - final static int[] PROXIMITY = { + static final int[] PROXIMITY = { // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter, // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's. // The number of rows must be exactly PROXIMITY_GRID_HEIGHT. @@ -419,37 +410,37 @@ public final class SpellCheckerProximityInfo { NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, }; + + static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE); + static { - buildProximityIndices(PROXIMITY, INDICES); - } - static int getIndexOf(int characterCode) { - return computeIndex(characterCode, INDICES); + buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES); } } - public static int[] getProximityForScript(final int script) { + private static int[] getProximityForScript(final int script) { switch (script) { - case AndroidSpellCheckerService.SCRIPT_LATIN: - return Latin.PROXIMITY; - case AndroidSpellCheckerService.SCRIPT_CYRILLIC: - return Cyrillic.PROXIMITY; - case AndroidSpellCheckerService.SCRIPT_GREEK: - return Greek.PROXIMITY; - default: - throw new RuntimeException("Wrong script supplied: " + script); + case AndroidSpellCheckerService.SCRIPT_LATIN: + return Latin.PROXIMITY; + case AndroidSpellCheckerService.SCRIPT_CYRILLIC: + return Cyrillic.PROXIMITY; + case AndroidSpellCheckerService.SCRIPT_GREEK: + return Greek.PROXIMITY; + default: + throw new RuntimeException("Wrong script supplied: " + script); } } private static int getIndexOfCodeForScript(final int codePoint, final int script) { switch (script) { - case AndroidSpellCheckerService.SCRIPT_LATIN: - return Latin.getIndexOf(codePoint); - case AndroidSpellCheckerService.SCRIPT_CYRILLIC: - return Cyrillic.getIndexOf(codePoint); - case AndroidSpellCheckerService.SCRIPT_GREEK: - return Greek.getIndexOf(codePoint); - default: - throw new RuntimeException("Wrong script supplied: " + script); + case AndroidSpellCheckerService.SCRIPT_LATIN: + return Latin.INDICES.get(codePoint, NOT_AN_INDEX); + case AndroidSpellCheckerService.SCRIPT_CYRILLIC: + return Cyrillic.INDICES.get(codePoint, NOT_AN_INDEX); + case AndroidSpellCheckerService.SCRIPT_GREEK: + return Greek.INDICES.get(codePoint, NOT_AN_INDEX); + default: + throw new RuntimeException("Wrong script supplied: " + script); } } diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java index ed408bb3c..3037669c0 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java @@ -16,12 +16,14 @@ package com.android.inputmethod.latin.suggestions; +import android.content.Context; import android.content.res.Resources; import android.graphics.Paint; import android.graphics.drawable.Drawable; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.keyboard.TypefaceUtils; import com.android.inputmethod.keyboard.internal.KeyboardBuilder; import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; import com.android.inputmethod.keyboard.internal.KeyboardParams; @@ -50,16 +52,12 @@ public final class MoreSuggestions extends Keyboard { super(); } - // TODO: Remove {@link MoreSuggestionsView} argument. public int layout(final SuggestedWords suggestions, final int fromPos, final int maxWidth, - final int minWidth, final int maxRow, final MoreSuggestionsView view) { + final int minWidth, final int maxRow, final Paint paint, final Resources res) { clearKeys(); - final Resources res = view.getResources(); mDivider = res.getDrawable(R.drawable.more_suggestions_divider); mDividerWidth = mDivider.getIntrinsicWidth(); - final int padding = (int) res.getDimension( - R.dimen.more_suggestions_key_horizontal_padding); - final Paint paint = view.newDefaultLabelPaint(); + final float padding = res.getDimension(R.dimen.more_suggestions_key_horizontal_padding); int row = 0; int pos = fromPos, rowStartPos = fromPos; @@ -67,7 +65,7 @@ public final class MoreSuggestions extends Keyboard { while (pos < size) { final String word = suggestions.getWord(pos); // TODO: Should take care of text x-scaling. - mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding; + mWidths[pos] = (int)(TypefaceUtils.getLabelWidth(word, paint) + padding); final int numColumn = pos - rowStartPos + 1; final int columnWidth = (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn; @@ -169,8 +167,8 @@ public final class MoreSuggestions extends Keyboard { private int mFromPos; private int mToPos; - public Builder(final MoreSuggestionsView paneView) { - super(paneView.getContext(), new MoreSuggestionsParam()); + public Builder(final Context context, final MoreSuggestionsView paneView) { + super(context, new MoreSuggestionsParam()); mPaneView = paneView; } @@ -183,7 +181,7 @@ public final class MoreSuggestions extends Keyboard { mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight); final int count = mParams.layout(suggestions, fromPos, maxWidth, minWidth, maxRow, - mPaneView); + mPaneView.newLabelPaint(null /* key */), mResources); mFromPos = fromPos; mToPos = fromPos + count; mSuggestions = suggestions; diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java index 438820d17..6509f394b 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java @@ -43,7 +43,13 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView { } public void updateKeyboardGeometry(final int keyHeight) { - mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes); + updateKeyDrawParams(keyHeight); + } + + public void adjustVerticalCorrectionForModalMode() { + // Set vertical correction to zero (Reset more keys keyboard sliding allowance + // {@link R#dimen.more_keys_keyboard_slide_allowance}). + mKeyDetector.setKeyboard(getKeyboard(), -getPaddingLeft(), -getPaddingTop()); } @Override diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index eeaf828a7..2a21ec2f5 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -596,7 +596,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null); mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer .findViewById(R.id.more_suggestions_view); - mMoreSuggestionsBuilder = new MoreSuggestions.Builder(mMoreSuggestionsView); + mMoreSuggestionsBuilder = new MoreSuggestions.Builder(context, mMoreSuggestionsView); final Resources res = context.getResources(); mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset( @@ -755,8 +755,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick @Override public boolean dispatchTouchEvent(final MotionEvent me) { - if (!mMoreSuggestionsView.isShowingInParent() - || mMoreSuggestionsMode == MORE_SUGGESTIONS_IN_MODAL_MODE) { + if (!mMoreSuggestionsView.isShowingInParent()) { mLastX = (int)me.getX(); mLastY = (int)me.getY(); if (mMoreSuggestionsSlidingDetector.onTouchEvent(me)) { @@ -784,6 +783,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { // Decided to be in the modal input mode mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE; + mMoreSuggestionsView.adjustVerticalCorrectionForModalMode(); } return true; } diff --git a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java index c5f095919..4f86526a7 100644 --- a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java +++ b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java @@ -25,9 +25,10 @@ import android.content.Intent; */ public final class BootBroadcastReceiver extends BroadcastReceiver { @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(final Context context, final Intent intent) { if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { - ResearchLogger.scheduleUploadingService(context); + UploaderService.cancelAndRescheduleUploadingService(context, + true /* needsRescheduling */); } } } diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java index e1cc2da73..fbfd9b531 100644 --- a/java/src/com/android/inputmethod/research/MotionEventReader.java +++ b/java/src/com/android/inputmethod/research/MotionEventReader.java @@ -22,6 +22,7 @@ import android.view.MotionEvent; import android.view.MotionEvent.PointerCoords; import android.view.MotionEvent.PointerProperties; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.define.ProductionFlag; import java.io.BufferedReader; @@ -64,6 +65,7 @@ public class MotionEventReader { return replayData; } + @UsedForTesting static class ReplayData { final ArrayList<Integer> mActions = new ArrayList<Integer>(); final ArrayList<PointerProperties[]> mPointerPropertiesArrays @@ -134,6 +136,7 @@ public class MotionEventReader { * }, * </pre> */ + @UsedForTesting /* package for test */ void readLogStatement(final JsonReader jsonReader, final ReplayData replayData) throws IOException { String logStatementType = null; diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index fa124f3ba..7a23ddb05 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -20,16 +20,13 @@ import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOAR import android.accounts.Account; import android.accounts.AccountManager; -import android.app.AlarmManager; import android.app.AlertDialog; import android.app.Dialog; -import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; @@ -74,22 +71,17 @@ import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.research.MotionEventReader.ReplayData; -import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStreamReader; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.List; -import java.util.Locale; import java.util.Random; -import java.util.UUID; +import java.util.regex.Pattern; /** * Logs the use of the LatinIME keyboard. @@ -254,7 +246,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mUploadNowIntent = new Intent(mLatinIME, UploaderService.class); mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - scheduleUploadingService(mLatinIME); + UploaderService.cancelAndRescheduleUploadingService(mLatinIME, + true /* needsRescheduling */); } mReplayer.setKeyboardSwitcher(keyboardSwitcher); } @@ -268,25 +261,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang ResearchSettings.writeResearchLastDirCleanupTime(mPrefs, now); } - /** - * Arrange for the UploaderService to be run on a regular basis. - * - * Any existing scheduled invocation of UploaderService is removed and rescheduled. This may - * cause problems if this method is called often and frequent updates are required, but since - * the user will likely be sleeping at some point, if the interval is less that the expected - * sleep duration and this method is not called during that time, the service should be invoked - * at some point. - */ - public static void scheduleUploadingService(Context context) { - final Intent intent = new Intent(context, UploaderService.class); - final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); - final AlarmManager manager = - (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - manager.cancel(pendingIntent); - manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, - UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent); - } - public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) { mMainKeyboardView = mainKeyboardView; maybeShowSplashScreen(); @@ -406,6 +380,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang requestIndicatorRedraw(); mStatistics.reset(); checkForEmptyEditor(); + if (mFeedbackLogBuffer == null) { + resetFeedbackLogging(); + } if (!isAllowedToLog()) { // Log.w(TAG, "not in usability mode; not logging"); return; @@ -439,9 +416,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } }; } - if (mFeedbackLogBuffer == null) { - resetFeedbackLogging(); - } } private void resetFeedbackLogging() { @@ -1092,7 +1066,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid", "packageName", "inputType", "imeOptions", "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything", - "isUsingDevelopmentOnlyDiagnosticsDebug"); + "isDevTeamBuild"); public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, final SharedPreferences prefs) { final ResearchLogger researchLogger = getInstance(); @@ -1114,13 +1088,32 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING, - ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG); - } catch (NameNotFoundException e) { - e.printStackTrace(); + researchLogger.isDevTeamBuild()); + // Commit the logUnit so the LatinImeOnStartInputViewInternal event is in its own + // logUnit at the beginning of the log. + researchLogger.commitCurrentLogUnit(); + } catch (final NameNotFoundException e) { + Log.e(TAG, "NameNotFound", e); } } } + // TODO: Update this heuristic pattern to something more reliable. Developer builds tend to + // have the developer name and year embedded. + private static final Pattern developerBuildRegex = Pattern.compile("[A-Za-z]\\.20[1-9]"); + private boolean isDevTeamBuild() { + try { + final PackageInfo packageInfo; + packageInfo = mLatinIME.getPackageManager().getPackageInfo(mLatinIME.getPackageName(), + 0); + final String versionName = packageInfo.versionName; + return !(developerBuildRegex.matcher(versionName).find()); + } catch (final NameNotFoundException e) { + Log.e(TAG, "Could not determine package name", e); + return false; + } + } + /** * Log a change in preferences. * @@ -1290,7 +1283,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (connection != null) { Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1); if (range != null) { - word = range.mWord; + word = range.mWord.toString(); } } final ResearchLogger researchLogger = getInstance(); diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java index 6a9f5c1f4..6a9717b7c 100644 --- a/java/src/com/android/inputmethod/research/UploaderService.java +++ b/java/src/com/android/inputmethod/research/UploaderService.java @@ -18,6 +18,8 @@ package com.android.inputmethod.research; import android.app.AlarmManager; import android.app.IntentService; +import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -43,11 +45,17 @@ public final class UploaderService extends IntentService { @Override protected void onHandleIntent(final Intent intent) { + // We may reach this point either because the alarm fired, or because the system explicitly + // requested that an Upload occur. In the latter case, we want to cancel the alarm in case + // it's about to fire. + cancelAndRescheduleUploadingService(this, false /* needsRescheduling */); + final Uploader uploader = new Uploader(this); if (!uploader.isPossibleToUpload()) return; if (isUploadingUnconditionally(intent.getExtras()) || uploader.isConvenientToUpload()) { uploader.doUpload(); } + cancelAndRescheduleUploadingService(this, true /* needsRescheduling */); } private boolean isUploadingUnconditionally(final Bundle bundle) { @@ -57,4 +65,42 @@ public final class UploaderService extends IntentService { } return false; } + + /** + * Arrange for the UploaderService to be run on a regular basis. + * + * Any existing scheduled invocation of UploaderService is removed and optionally rescheduled. + * This may cause problems if this method is called so often that no scheduled invocation is + * ever run. But if the delay is short enough that it will go off when the user is sleeping, + * then there should be no starvation. + * + * @param context {@link Context} object + * @param needsRescheduling whether to schedule a future intent to be delivered to this service + */ + public static void cancelAndRescheduleUploadingService(final Context context, + final boolean needsRescheduling) { + final PendingIntent pendingIntent = getPendingIntentForService(context); + final AlarmManager alarmManager = (AlarmManager) context.getSystemService( + Context.ALARM_SERVICE); + cancelAnyScheduledServiceAlarm(alarmManager, pendingIntent); + if (needsRescheduling) { + scheduleServiceAlarm(alarmManager, pendingIntent); + } + } + + private static PendingIntent getPendingIntentForService(final Context context) { + final Intent intent = new Intent(context, UploaderService.class); + return PendingIntent.getService(context, 0, intent, 0); + } + + private static void cancelAnyScheduledServiceAlarm(final AlarmManager alarmManager, + final PendingIntent pendingIntent) { + alarmManager.cancel(pendingIntent); + } + + private static void scheduleServiceAlarm(final AlarmManager alarmManager, + final PendingIntent pendingIntent) { + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, UploaderService.RUN_INTERVAL, + pendingIntent); + } } |