diff options
Diffstat (limited to 'java/src')
18 files changed, 320 insertions, 184 deletions
diff --git a/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java b/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java index 3a86ccbda..8a2818508 100644 --- a/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java +++ b/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java @@ -34,10 +34,15 @@ public final class CursorAnchorInfoCompatWrapper { */ public static final int FLAG_HAS_INVISIBLE_REGION = 0x02; + /** + * The insertion marker or character bounds is placed at right-to-left (RTL) character. + */ + public static final int FLAG_IS_RTL = 0x04; + // Note that CursorAnchorInfo has been introduced in API level XX (Build.VERSION_CODE.LXX). private static final CompatUtils.ClassWrapper sCursorAnchorInfoClass; - private static final CompatUtils.ToObjectMethodWrapper<RectF> sGetCharacterRectMethod; - private static final CompatUtils.ToIntMethodWrapper sGetCharacterRectFlagsMethod; + private static final CompatUtils.ToObjectMethodWrapper<RectF> sGetCharacterBoundsMethod; + private static final CompatUtils.ToIntMethodWrapper sGetCharacterBoundsFlagsMethod; private static final CompatUtils.ToObjectMethodWrapper<CharSequence> sGetComposingTextMethod; private static final CompatUtils.ToIntMethodWrapper sGetComposingTextStartMethod; private static final CompatUtils.ToFloatMethodWrapper sGetInsertionMarkerBaselineMethod; @@ -51,10 +56,10 @@ public final class CursorAnchorInfoCompatWrapper { static { sCursorAnchorInfoClass = CompatUtils.getClassWrapper( "android.view.inputmethod.CursorAnchorInfo"); - sGetCharacterRectMethod = sCursorAnchorInfoClass.getMethod( - "getCharacterRect", (RectF)null, int.class); - sGetCharacterRectFlagsMethod = sCursorAnchorInfoClass.getPrimitiveMethod( - "getCharacterRectFlags", 0, int.class); + sGetCharacterBoundsMethod = sCursorAnchorInfoClass.getMethod( + "getCharacterBounds", (RectF)null, int.class); + sGetCharacterBoundsFlagsMethod = sCursorAnchorInfoClass.getPrimitiveMethod( + "getCharacterBoundsFlags", 0, int.class); sGetComposingTextMethod = sCursorAnchorInfoClass.getMethod( "getComposingText", (CharSequence)null); sGetComposingTextStartMethod = sCursorAnchorInfoClass.getPrimitiveMethod( @@ -112,12 +117,12 @@ public final class CursorAnchorInfoCompatWrapper { return sGetMatrixMethod.invoke(mInstance); } - public RectF getCharacterRect(final int index) { - return sGetCharacterRectMethod.invoke(mInstance, index); + public RectF getCharacterBounds(final int index) { + return sGetCharacterBoundsMethod.invoke(mInstance, index); } - public int getCharacterRectFlags(final int index) { - return sGetCharacterRectFlagsMethod.invoke(mInstance, index); + public int getCharacterBoundsFlags(final int index) { + return sGetCharacterBoundsFlagsMethod.invoke(mInstance, index); } public float getInsertionMarkerBaseline() { diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index efa527e15..a6f9f3c26 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -547,6 +547,10 @@ public class Key implements Comparable<Key> { return this instanceof Spacer; } + public final boolean isActionKey() { + return mBackgroundType == BACKGROUND_TYPE_ACTION; + } + public final boolean isShift() { return mCode == CODE_SHIFT; } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index 91d703330..28f2dcf89 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -39,6 +39,7 @@ import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; import com.android.inputmethod.latin.SubtypeSwitcher; import com.android.inputmethod.latin.WordComposer; +import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.utils.ResourceUtils; import com.android.inputmethod.latin.utils.ScriptUtils; @@ -61,7 +62,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { private KeyboardLayoutSet mKeyboardLayoutSet; // TODO: The following {@link KeyboardTextsSet} should be in {@link KeyboardLayoutSet}. private final KeyboardTextsSet mKeyboardTextsSet = new KeyboardTextsSet(); - private SettingsValues mCurrentSettingsValues; private KeyboardTheme mKeyboardTheme; private Context mThemeContext; @@ -121,7 +121,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { builder.setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey); builder.setLanguageSwitchKeyEnabled(mLatinIME.shouldShowLanguageSwitchKey()); mKeyboardLayoutSet = builder.build(); - mCurrentSettingsValues = settingsValues; try { mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState); mKeyboardTextsSet.setLocale(mSubtypeSwitcher.getCurrentSubtypeLocale(), mThemeContext); @@ -145,22 +144,24 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { private void setKeyboard(final Keyboard keyboard) { // Make {@link MainKeyboardView} visible and hide {@link EmojiPalettesView}. - setMainKeyboardFrame(); + final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent(); + setMainKeyboardFrame(currentSettingsValues); + // TODO: pass this object to setKeyboard instead of getting the current values. final MainKeyboardView keyboardView = mKeyboardView; final Keyboard oldKeyboard = keyboardView.getKeyboard(); keyboardView.setKeyboard(keyboard); mCurrentInputView.setKeyboardTopPadding(keyboard.mTopPadding); keyboardView.setKeyPreviewPopupEnabled( - mCurrentSettingsValues.mKeyPreviewPopupOn, - mCurrentSettingsValues.mKeyPreviewPopupDismissDelay); + currentSettingsValues.mKeyPreviewPopupOn, + currentSettingsValues.mKeyPreviewPopupDismissDelay); keyboardView.setKeyPreviewAnimationParams( - mCurrentSettingsValues.mHasCustomKeyPreviewAnimationParams, - mCurrentSettingsValues.mKeyPreviewShowUpStartXScale, - mCurrentSettingsValues.mKeyPreviewShowUpStartYScale, - mCurrentSettingsValues.mKeyPreviewShowUpDuration, - mCurrentSettingsValues.mKeyPreviewDismissEndXScale, - mCurrentSettingsValues.mKeyPreviewDismissEndYScale, - mCurrentSettingsValues.mKeyPreviewDismissDuration); + currentSettingsValues.mHasCustomKeyPreviewAnimationParams, + currentSettingsValues.mKeyPreviewShowUpStartXScale, + currentSettingsValues.mKeyPreviewShowUpStartYScale, + currentSettingsValues.mKeyPreviewShowUpDuration, + currentSettingsValues.mKeyPreviewDismissEndXScale, + currentSettingsValues.mKeyPreviewDismissEndYScale, + currentSettingsValues.mKeyPreviewDismissDuration); keyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady()); final boolean subtypeChanged = (oldKeyboard == null) || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale); @@ -237,22 +238,13 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_SYMBOLS)); } - private void setMainKeyboardFrame() { - mMainKeyboardFrame.setVisibility(hasHardwareKeyboard() ? View.GONE : View.VISIBLE); + private void setMainKeyboardFrame(final SettingsValues settingsValues) { + mMainKeyboardFrame.setVisibility( + settingsValues.mHasHardwareKeyboard ? View.GONE : View.VISIBLE); mEmojiPalettesView.setVisibility(View.GONE); mEmojiPalettesView.stopEmojiPalettes(); } - // TODO: Move this boolean to a member of {@link SettingsValues} and reset it - // at {@link LatinIME#onConfigurationChanged(Configuration)}. - public boolean hasHardwareKeyboard() { - // Copied from {@link InputMethodServce#onEvaluateInputViewShown()}. - final Configuration config = mLatinIME.getResources().getConfiguration(); - final boolean noHardwareKeyboard = config.keyboard == Configuration.KEYBOARD_NOKEYS - || config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES; - return !noHardwareKeyboard; - } - // Implements {@link KeyboardState.SwitchActions}. @Override public void setEmojiKeyboard() { diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index d2f3e9714..2ed4ea98e 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -88,6 +88,7 @@ import java.util.WeakHashMap; * @attr ref R.styleable#MainKeyboardView_keyPreviewShowUpAnimator * @attr ref R.styleable#MainKeyboardView_keyPreviewDismissAnimator * @attr ref R.styleable#MainKeyboardView_moreKeysKeyboardLayout + * @attr ref R.styleable#MainKeyboardView_moreKeysKeyboardForActionLayout * @attr ref R.styleable#MainKeyboardView_backgroundDimAlpha * @attr ref R.styleable#MainKeyboardView_showMoreKeysKeyboardAtTouchPoint * @attr ref R.styleable#MainKeyboardView_gestureFloatingPreviewTextLingerTimeout @@ -147,6 +148,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack private final Paint mBackgroundDimAlphaPaint = new Paint(); private boolean mNeedsToDimEntireKeyboard; private final View mMoreKeysKeyboardContainer; + private final View mMoreKeysKeyboardForActionContainer; private final WeakHashMap<Key, Keyboard> mMoreKeysKeyboardCache = new WeakHashMap<>(); private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint; // More keys panel (used by both more keys keyboard and more suggestions view) @@ -231,6 +233,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final int moreKeysKeyboardLayoutId = mainKeyboardViewAttr.getResourceId( R.styleable.MainKeyboardView_moreKeysKeyboardLayout, 0); + final int moreKeysKeyboardForActionLayoutId = mainKeyboardViewAttr.getResourceId( + R.styleable.MainKeyboardView_moreKeysKeyboardForActionLayout, + moreKeysKeyboardLayoutId); mConfigShowMoreKeysKeyboardAtTouchedPoint = mainKeyboardViewAttr.getBoolean( R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false); @@ -248,8 +253,10 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mSlidingKeyInputDrawingPreview.setDrawingView(mDrawingPreviewPlacerView); mainKeyboardViewAttr.recycle(); - mMoreKeysKeyboardContainer = LayoutInflater.from(getContext()) - .inflate(moreKeysKeyboardLayoutId, null); + final LayoutInflater inflater = LayoutInflater.from(getContext()); + mMoreKeysKeyboardContainer = inflater.inflate(moreKeysKeyboardLayoutId, null); + mMoreKeysKeyboardForActionContainer = inflater.inflate( + moreKeysKeyboardForActionLayoutId, null); mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator( languageOnSpacebarFadeoutAnimatorResId, this); mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator( @@ -581,7 +588,8 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mMoreKeysKeyboardCache.put(key, moreKeysKeyboard); } - final View container = mMoreKeysKeyboardContainer; + final View container = key.isActionKey() ? mMoreKeysKeyboardForActionContainer + : mMoreKeysKeyboardContainer; final MoreKeysKeyboardView moreKeysKeyboardView = (MoreKeysKeyboardView)container.findViewById(R.id.more_keys_keyboard_view); moreKeysKeyboardView.setKeyboard(moreKeysKeyboard); diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java index 52e2e85ba..73c84cd92 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java @@ -18,11 +18,9 @@ package com.android.inputmethod.keyboard; import android.content.Context; import android.graphics.Paint; -import android.graphics.drawable.Drawable; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.internal.KeyboardBuilder; -import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; import com.android.inputmethod.keyboard.internal.KeyboardParams; import com.android.inputmethod.keyboard.internal.MoreKeySpec; import com.android.inputmethod.latin.R; @@ -257,7 +255,6 @@ public final class MoreKeysKeyboard extends Keyboard { public static class Builder extends KeyboardBuilder<MoreKeysKeyboardParams> { private final Key mParentKey; - private final Drawable mDivider; private static final float LABEL_PADDING_RATIO = 0.2f; private static final float DIVIDER_RATIO = 0.2f; @@ -306,10 +303,8 @@ public final class MoreKeysKeyboard extends Keyboard { } final int dividerWidth; if (key.needsDividersInMoreKeys()) { - mDivider = mResources.getDrawable(R.drawable.more_keys_divider); dividerWidth = (int)(keyWidth * DIVIDER_RATIO); } else { - mDivider = null; dividerWidth = 0; } final MoreKeySpec[] moreKeys = key.getMoreKeys(); @@ -352,7 +347,8 @@ public final class MoreKeysKeyboard extends Keyboard { if (params.mDividerWidth > 0 && pos != 0) { final int dividerX = (pos > 0) ? x - params.mDividerWidth : x + params.mDefaultKeyWidth; - final Key divider = new MoreKeyDivider(params, mDivider, dividerX, y); + final Key divider = new MoreKeyDivider( + params, dividerX, y, params.mDividerWidth, params.mDefaultRowHeight); params.onAddKey(divider); } } @@ -360,22 +356,11 @@ public final class MoreKeysKeyboard extends Keyboard { } } - private static class MoreKeyDivider extends Key.Spacer { - private final Drawable mIcon; - - public MoreKeyDivider(final MoreKeysKeyboardParams params, final Drawable icon, - final int x, final int y) { - super(params, x, y, params.mDividerWidth, params.mDefaultRowHeight); - mIcon = icon; - } - - @Override - public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) { - // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the - // constructor. - // TODO: Drawable itself should have an alpha value. - mIcon.setAlpha(128); - return mIcon; + // Used as a divider maker. A divider is drawn by {@link MoreKeysKeyboardView}. + public static class MoreKeyDivider extends Key.Spacer { + public MoreKeyDivider(final KeyboardParams params, final int x, final int y, + final int width, final int height) { + super(params, x, y, width, height); } } } diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java index 5140c4ffc..a9d1239ed 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java @@ -17,6 +17,10 @@ package com.android.inputmethod.keyboard; import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -24,6 +28,7 @@ import android.view.ViewGroup; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.accessibility.MoreKeysKeyboardAccessibilityDelegate; +import com.android.inputmethod.keyboard.internal.KeyDrawParams; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.CoordinateUtils; @@ -35,6 +40,7 @@ import com.android.inputmethod.latin.utils.CoordinateUtils; public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel { private final int[] mCoordinates = CoordinateUtils.newInstance(); + private final Drawable mDivider; protected final KeyDetector mKeyDetector; private Controller mController = EMPTY_CONTROLLER; protected KeyboardActionListener mListener; @@ -53,6 +59,14 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel public MoreKeysKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); + final TypedArray moreKeysKeyboardViewAttr = context.obtainStyledAttributes(attrs, + R.styleable.MoreKeysKeyboardView, defStyle, R.style.MoreKeysKeyboardView); + mDivider = moreKeysKeyboardViewAttr.getDrawable(R.styleable.MoreKeysKeyboardView_divider); + if (mDivider != null) { + // TODO: Drawable itself should have an alpha value. + mDivider.setAlpha(128); + } + moreKeysKeyboardViewAttr.recycle(); mKeyDetector = new MoreKeysDetector(getResources().getDimension( R.dimen.config_more_keys_keyboard_slide_allowance)); } @@ -70,6 +84,23 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel } @Override + protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint, + final KeyDrawParams params) { + if (!key.isSpacer() || !(key instanceof MoreKeysKeyboard.MoreKeyDivider) + || mDivider == null) { + super.onDrawKeyTopVisuals(key, canvas, paint, params); + return; + } + final int keyWidth = key.getDrawWidth(); + final int keyHeight = key.getHeight(); + final int iconWidth = Math.min(mDivider.getIntrinsicWidth(), keyWidth); + final int iconHeight = mDivider.getIntrinsicHeight(); + final int iconX = (keyWidth - iconWidth) / 2; // Align horizontally center + final int iconY = (keyHeight - iconHeight) / 2; // Align vertically center + drawIcon(canvas, mDivider, iconX, iconY, iconWidth, iconHeight); + } + + @Override public void setKeyboard(final Keyboard keyboard) { super.setKeyboard(keyboard); mKeyDetector.setKeyboard( diff --git a/java/src/com/android/inputmethod/keyboard/TextDecorator.java b/java/src/com/android/inputmethod/keyboard/TextDecorator.java index 9192853ca..cf58d6a09 100644 --- a/java/src/com/android/inputmethod/keyboard/TextDecorator.java +++ b/java/src/com/android/inputmethod/keyboard/TextDecorator.java @@ -274,8 +274,8 @@ public class TextDecorator { } final int composingTextStart = info.getComposingTextStart(); final int lastCharRectIndex = composingTextStart + composingText.length() - 1; - final RectF lastCharRect = info.getCharacterRect(lastCharRectIndex); - final int lastCharRectFlag = info.getCharacterRectFlags(lastCharRectIndex); + final RectF lastCharRect = info.getCharacterBounds(lastCharRectIndex); + final int lastCharRectFlag = info.getCharacterBoundsFlags(lastCharRectIndex); final boolean hasInvisibleRegionInLastCharRect = (lastCharRectFlag & CursorAnchorInfoCompatWrapper.FLAG_HAS_INVISIBLE_REGION) != 0; @@ -285,7 +285,7 @@ public class TextDecorator { } final RectF segmentStartCharRect = new RectF(lastCharRect); for (int i = composingText.length() - 2; i >= 0; --i) { - final RectF charRect = info.getCharacterRect(composingTextStart + i); + final RectF charRect = info.getCharacterBounds(composingTextStart + i); if (charRect == null) { break; } diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 02d5eddaa..fc7f95c7b 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -230,7 +230,6 @@ public final class Constants { public static final String REGEXP_PERIOD = "\\."; public static final String STRING_SPACE = " "; - public static final String STRING_PERIOD_AND_SPACE = ". "; /** * Special keys code. Must be negative. diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java index 480bd1f85..fde94da93 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -62,10 +62,10 @@ public class DictionaryFacilitator { private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140; private static final int MAX_DICTIONARY_FACILITATOR_CACHE_SIZE = 3; - private Dictionaries mDictionaries = new Dictionaries(); + private DictionaryGroup mDictionaryGroup = new DictionaryGroup(); private boolean mIsUserDictEnabled = false; private volatile CountDownLatch mLatchForWaitingLoadingMainDictionary = new CountDownLatch(0); - // To synchronize assigning mDictionaries to ensure closing dictionaries. + // To synchronize assigning mDictionaryGroup to ensure closing dictionaries. private final Object mLock = new Object(); private final DistracterFilter mDistracterFilter; private final DictionaryFacilitatorLruCache mFacilitatorCacheForPersonalization; @@ -100,22 +100,22 @@ public class DictionaryFacilitator { DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS.length); /** - * Class contains dictionaries for a locale. + * A group of dictionaries that work together for a single language. */ - private static class Dictionaries { + private static class DictionaryGroup { public final Locale mLocale; private Dictionary mMainDict; public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap = new ConcurrentHashMap<>(); - public Dictionaries() { + public DictionaryGroup() { mLocale = null; } - public Dictionaries(final Locale locale, final Dictionary mainDict, + public DictionaryGroup(final Locale locale, final Dictionary mainDict, final Map<String, ExpandableBinaryDictionary> subDicts) { mLocale = locale; - // Main dictionary can be asynchronously loaded. + // The main dictionary can be asynchronously loaded. setMainDict(mainDict); for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) { setSubDict(entry.getKey(), entry.getValue()); @@ -191,7 +191,7 @@ public class DictionaryFacilitator { } public Locale getLocale() { - return mDictionaries.mLocale; + return mDictionaryGroup.mLocale; } private static ExpandableBinaryDictionary getSubDict(final String dictType, @@ -228,7 +228,7 @@ public class DictionaryFacilitator { final boolean forceReloadMainDictionary, final DictionaryInitializationListener listener, final String dictNamePrefix) { - final boolean localeHasBeenChanged = !newLocale.equals(mDictionaries.mLocale); + final boolean localeHasBeenChanged = !newLocale.equals(mDictionaryGroup.mLocale); // We always try to have the main dictionary. Other dictionaries can be unused. final boolean reloadMainDictionary = localeHasBeenChanged || forceReloadMainDictionary; // TODO: Make subDictTypesToUse configurable by resource or a static final list. @@ -248,7 +248,7 @@ public class DictionaryFacilitator { // The main dictionary will be asynchronously loaded. newMainDict = null; } else { - newMainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN); + newMainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN); } final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); @@ -258,9 +258,9 @@ public class DictionaryFacilitator { continue; } final ExpandableBinaryDictionary dict; - if (!localeHasBeenChanged && mDictionaries.hasDict(dictType)) { + if (!localeHasBeenChanged && mDictionaryGroup.hasDict(dictType)) { // Continue to use current dictionary. - dict = mDictionaries.getSubDict(dictType); + dict = mDictionaryGroup.getSubDict(dictType); } else { // Start to use new dictionary. dict = getSubDict(dictType, context, newLocale, null /* dictFile */, @@ -269,12 +269,12 @@ public class DictionaryFacilitator { subDicts.put(dictType, dict); } - // Replace Dictionaries. - final Dictionaries newDictionaries = new Dictionaries(newLocale, newMainDict, subDicts); - final Dictionaries oldDictionaries; + // Replace DictionaryGroup. + final DictionaryGroup newDictionaryGroup = new DictionaryGroup(newLocale, newMainDict, subDicts); + final DictionaryGroup oldDictionaryGroup; synchronized (mLock) { - oldDictionaries = mDictionaries; - mDictionaries = newDictionaries; + oldDictionaryGroup = mDictionaryGroup; + mDictionaryGroup = newDictionaryGroup; mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context); if (reloadMainDictionary) { asyncReloadMainDictionary(context, newLocale, listener); @@ -285,14 +285,14 @@ public class DictionaryFacilitator { } // Clean up old dictionaries. if (reloadMainDictionary) { - oldDictionaries.closeDict(Dictionary.TYPE_MAIN); + oldDictionaryGroup.closeDict(Dictionary.TYPE_MAIN); } for (final String dictType : SUB_DICT_TYPES) { if (localeHasBeenChanged || !subDictTypesToUse.contains(dictType)) { - oldDictionaries.closeDict(dictType); + oldDictionaryGroup.closeDict(dictType); } } - oldDictionaries.mSubDictMap.clear(); + oldDictionaryGroup.mSubDictMap.clear(); } private void asyncReloadMainDictionary(final Context context, final Locale locale, @@ -305,8 +305,8 @@ public class DictionaryFacilitator { final Dictionary mainDict = DictionaryFactory.createMainDictionaryFromManager(context, locale); synchronized (mLock) { - if (locale.equals(mDictionaries.mLocale)) { - mDictionaries.setMainDict(mainDict); + if (locale.equals(mDictionaryGroup.mLocale)) { + mDictionaryGroup.setMainDict(mainDict); } else { // Dictionary facilitator has been reset for another locale. mainDict.close(); @@ -346,17 +346,17 @@ public class DictionaryFacilitator { subDicts.put(dictType, dict); } } - mDictionaries = new Dictionaries(locale, mainDictionary, subDicts); + mDictionaryGroup = new DictionaryGroup(locale, mainDictionary, subDicts); } public void closeDictionaries() { - final Dictionaries dictionaries; + final DictionaryGroup dictionaryGroup; synchronized (mLock) { - dictionaries = mDictionaries; - mDictionaries = new Dictionaries(); + dictionaryGroup = mDictionaryGroup; + mDictionaryGroup = new DictionaryGroup(); } for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - dictionaries.closeDict(dictType); + dictionaryGroup.closeDict(dictType); } if (mFacilitatorCacheForPersonalization != null) { mFacilitatorCacheForPersonalization.evictAll(); @@ -366,23 +366,23 @@ public class DictionaryFacilitator { @UsedForTesting public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { - return mDictionaries.getSubDict(dictName); + return mDictionaryGroup.getSubDict(dictName); } // The main dictionary could have been loaded asynchronously. Don't cache the return value // of this method. public boolean hasInitializedMainDictionary() { - final Dictionary mainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN); + final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN); return mainDict != null && mainDict.isInitialized(); } public boolean hasPersonalizationDictionary() { - return mDictionaries.hasDict(Dictionary.TYPE_PERSONALIZATION); + return mDictionaryGroup.hasDict(Dictionary.TYPE_PERSONALIZATION); } public void flushPersonalizationDictionary() { final ExpandableBinaryDictionary personalizationDict = - mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION); + mDictionaryGroup.getSubDict(Dictionary.TYPE_PERSONALIZATION); if (personalizationDict != null) { personalizationDict.asyncFlushBinaryDictionary(); } @@ -397,7 +397,7 @@ public class DictionaryFacilitator { public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) throws InterruptedException { waitForLoadingMainDictionary(timeout, unit); - final Map<String, ExpandableBinaryDictionary> dictMap = mDictionaries.mSubDictMap; + final Map<String, ExpandableBinaryDictionary> dictMap = mDictionaryGroup.mSubDictMap; for (final ExpandableBinaryDictionary dict : dictMap.values()) { dict.waitAllTasksForTests(); } @@ -418,24 +418,24 @@ public class DictionaryFacilitator { public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, final PrevWordsInfo prevWordsInfo, final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { - final Dictionaries dictionaries = mDictionaries; + final DictionaryGroup dictionaryGroup = mDictionaryGroup; final String[] words = suggestion.split(Constants.WORD_SEPARATOR); PrevWordsInfo prevWordsInfoForCurrentWord = prevWordsInfo; for (int i = 0; i < words.length; i++) { final String currentWord = words[i]; final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false; - addWordToUserHistory(dictionaries, prevWordsInfoForCurrentWord, currentWord, + addWordToUserHistory(dictionaryGroup, prevWordsInfoForCurrentWord, currentWord, wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive); prevWordsInfoForCurrentWord = prevWordsInfoForCurrentWord.getNextPrevWordsInfo(new WordInfo(currentWord)); } } - private void addWordToUserHistory(final Dictionaries dictionaries, + private void addWordToUserHistory(final DictionaryGroup dictionaryGroup, final PrevWordsInfo prevWordsInfo, final String word, final boolean wasAutoCapitalized, final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { final ExpandableBinaryDictionary userHistoryDictionary = - dictionaries.getSubDict(Dictionary.TYPE_USER_HISTORY); + dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY); if (userHistoryDictionary == null) { return; } @@ -443,7 +443,7 @@ public class DictionaryFacilitator { if (maxFreq == 0 && blockPotentiallyOffensive) { return; } - final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); + final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); final String secondWord; if (wasAutoCapitalized) { if (isValidWord(word, false /* ignoreCase */) @@ -464,8 +464,8 @@ public class DictionaryFacilitator { // History dictionary in order to avoid suggesting them until the dictionary // consolidation is done. // TODO: Remove this hack when ready. - final int lowerCaseFreqInMainDict = dictionaries.hasDict(Dictionary.TYPE_MAIN) ? - dictionaries.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) : + final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN) ? + dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) : Dictionary.NOT_A_PROBABILITY; if (maxFreq < lowerCaseFreqInMainDict && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) { @@ -485,7 +485,7 @@ public class DictionaryFacilitator { } private void removeWord(final String dictName, final String word) { - final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); + final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); if (dictionary != null) { dictionary.removeUnigramEntryDynamically(word); } @@ -501,12 +501,12 @@ public class DictionaryFacilitator { public SuggestionResults getSuggestionResults(final WordComposer composer, final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) { - final Dictionaries dictionaries = mDictionaries; + final DictionaryGroup dictionaryGroup = mDictionaryGroup; final SuggestionResults suggestionResults = new SuggestionResults(SuggestedWords.MAX_SUGGESTIONS); final float[] languageWeight = new float[] { Dictionary.NOT_A_LANGUAGE_WEIGHT }; for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - final Dictionary dictionary = dictionaries.getDict(dictType); + final Dictionary dictionary = dictionaryGroup.getDict(dictType); if (null == dictionary) continue; final ArrayList<SuggestedWordInfo> dictionarySuggestions = dictionary.getSuggestions(composer, prevWordsInfo, proximityInfo, @@ -524,13 +524,13 @@ public class DictionaryFacilitator { if (TextUtils.isEmpty(word)) { return false; } - final Dictionaries dictionaries = mDictionaries; - if (dictionaries.mLocale == null) { + final DictionaryGroup dictionaryGroup = mDictionaryGroup; + if (dictionaryGroup.mLocale == null) { return false; } - final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); + final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - final Dictionary dictionary = dictionaries.getDict(dictType); + final Dictionary dictionary = dictionaryGroup.getDict(dictType); // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and // would be immutable once it's finished initializing, but concretely a null test is // probably good enough for the time being. @@ -549,9 +549,9 @@ public class DictionaryFacilitator { return Dictionary.NOT_A_PROBABILITY; } int maxFreq = Dictionary.NOT_A_PROBABILITY; - final Dictionaries dictionaries = mDictionaries; + final DictionaryGroup dictionaryGroup = mDictionaryGroup; for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - final Dictionary dictionary = dictionaries.getDict(dictType); + final Dictionary dictionary = dictionaryGroup.getDict(dictType); if (dictionary == null) continue; final int tempFreq; if (isGettingMaxFrequencyOfExactMatches) { @@ -575,7 +575,7 @@ public class DictionaryFacilitator { } private void clearSubDictionary(final String dictName) { - final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); + final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); if (dictionary != null) { dictionary.clear(); } @@ -600,7 +600,7 @@ public class DictionaryFacilitator { final SpacingAndPunctuations spacingAndPunctuations, final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) { final ExpandableBinaryDictionary personalizationDict = - mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION); + mDictionaryGroup.getSubDict(Dictionary.TYPE_PERSONALIZATION); if (personalizationDict == null) { if (callback != null) { callback.onFinished(); @@ -630,7 +630,7 @@ public class DictionaryFacilitator { public void addPhraseToContextualDictionary(final String[] phrase, final int probability, final int bigramProbabilityForWords, final int bigramProbabilityForPhrases) { final ExpandableBinaryDictionary contextualDict = - mDictionaries.getSubDict(Dictionary.TYPE_CONTEXTUAL); + mDictionaryGroup.getSubDict(Dictionary.TYPE_CONTEXTUAL); if (contextualDict == null) { return; } @@ -663,7 +663,7 @@ public class DictionaryFacilitator { } public void dumpDictionaryForDebug(final String dictName) { - final ExpandableBinaryDictionary dictToDump = mDictionaries.getSubDict(dictName); + final ExpandableBinaryDictionary dictToDump = mDictionaryGroup.getSubDict(dictName); if (dictToDump == null) { Log.e(TAG, "Cannot dump " + dictName + ". " + "The dictionary is not being used for suggestion or cannot be dumped."); @@ -674,9 +674,9 @@ public class DictionaryFacilitator { public ArrayList<Pair<String, DictionaryStats>> getStatsOfEnabledSubDicts() { final ArrayList<Pair<String, DictionaryStats>> statsOfEnabledSubDicts = new ArrayList<>(); - final Dictionaries dictionaries = mDictionaries; + final DictionaryGroup dictionaryGroup = mDictionaryGroup; for (final String dictType : SUB_DICT_TYPES) { - final ExpandableBinaryDictionary dictionary = dictionaries.getSubDict(dictType); + final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictType); if (dictionary == null) continue; statsOfEnabledSubDicts.add(new Pair<>(dictType, dictionary.getDictionaryStats())); } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index c55acd462..67e2ca5c7 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -713,11 +713,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onConfigurationChanged(final Configuration conf) { - final SettingsValues settingsValues = mSettings.getCurrent(); + SettingsValues settingsValues = mSettings.getCurrent(); if (settingsValues.mDisplayOrientation != conf.orientation) { mHandler.startOrientationChanging(); mInputLogic.onOrientationChange(mSettings.getCurrent()); } + if (settingsValues.mHasHardwareKeyboard != Settings.readHasHardwareKeyboard(conf)) { + // If the state of having a hardware keyboard changed, then we want to reload the + // settings to adjust for that. + // TODO: we should probably do this unconditionally here, rather than only when we + // have a change in hardware keyboard configuration. + loadSettings(); + settingsValues = mSettings.getCurrent(); + if (settingsValues.mHasHardwareKeyboard) { + // We call cleanupInternalStateForFinishInput() because it's the right thing to do; + // however, it seems at the moment the framework is passing us a seemingly valid + // but actually non-functional InputConnection object. So if this bug ever gets + // fixed we'll be able to remove the composition, but until it is this code is + // actually not doing much. + cleanupInternalStateForFinishInput(); + } + } // TODO: Remove this test. if (!conf.locale.equals(mPersonalizationDictionaryUpdater.getLocale())) { refreshPersonalizationDictionarySession(settingsValues); @@ -844,40 +860,52 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Note: This call should be done by InputMethodService? updateFullscreenMode(); - // The app calling setText() has the effect of clearing the composing - // span, so we should reset our state unconditionally, even if restarting is true. - // We also tell the input logic about the combining rules for the current subtype, so - // it can adjust its combiners if needed. - mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype(), - currentSettingsValues); + // ALERT: settings have not been reloaded and there is a chance they may be stale. + // In the practice, if it is, we should have gotten onConfigurationChanged so it should + // be fine, but this is horribly confusing and must be fixed AS SOON AS POSSIBLE. - // Note: the following does a round-trip IPC on the main thread: be careful - final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); + // In some cases the input connection has not been reset yet and we can't access it. In + // this case we will need to call loadKeyboard() later, when it's accessible, so that we + // can go into the correct mode, so we need to do some housekeeping here. + final boolean needToCallLoadKeyboardLater; final Suggest suggest = mInputLogic.mSuggest; - if (null != currentLocale && !currentLocale.equals(suggest.getLocale())) { - // TODO: Do this automatically. - resetSuggest(); - } + if (!currentSettingsValues.mHasHardwareKeyboard) { + // The app calling setText() has the effect of clearing the composing + // span, so we should reset our state unconditionally, even if restarting is true. + // We also tell the input logic about the combining rules for the current subtype, so + // it can adjust its combiners if needed. + mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype(), + currentSettingsValues); + + // Note: the following does a round-trip IPC on the main thread: be careful + final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); + if (null != currentLocale && !currentLocale.equals(suggest.getLocale())) { + // TODO: Do this automatically. + resetSuggest(); + } - // TODO[IL]: Can the following be moved to InputLogic#startInput? - final boolean canReachInputConnection; - if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( - editorInfo.initialSelStart, editorInfo.initialSelEnd, - false /* shouldFinishComposition */)) { - // Sometimes, while rotating, for some reason the framework tells the app we are not - // connected to it and that means we can't refresh the cache. In this case, schedule a - // refresh later. - // We try resetting the caches up to 5 times before giving up. - mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); - // mLastSelection{Start,End} are reset later in this method, don't need to do it here - canReachInputConnection = false; + // TODO[IL]: Can the following be moved to InputLogic#startInput? + if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( + editorInfo.initialSelStart, editorInfo.initialSelEnd, + false /* shouldFinishComposition */)) { + // Sometimes, while rotating, for some reason the framework tells the app we are not + // connected to it and that means we can't refresh the cache. In this case, schedule + // a refresh later. + // We try resetting the caches up to 5 times before giving up. + mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); + // mLastSelection{Start,End} are reset later in this method, no need to do it here + needToCallLoadKeyboardLater = true; + } else { + // When rotating, initialSelStart and initialSelEnd sometimes are lying. Make a best + // effort to work around this bug. + mInputLogic.mConnection.tryFixLyingCursorPosition(); + mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */, + true /* shouldDelay */); + needToCallLoadKeyboardLater = false; + } } else { - // When rotating, initialSelStart and initialSelEnd sometimes are lying. Make a best - // effort to work around this bug. - mInputLogic.mConnection.tryFixLyingCursorPosition(); - mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */, - true /* shouldDelay */); - canReachInputConnection = true; + // If we have a hardware keyboard we don't need to call loadKeyboard later anyway. + needToCallLoadKeyboardLater = false; } if (isDifferentTextField || @@ -895,9 +923,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); - if (!canReachInputConnection) { - // If we can't reach the input connection, we will call loadKeyboard again later, - // so we need to save its state now. The call will be done in #retryResetCaches. + if (needToCallLoadKeyboardLater) { + // If we need to call loadKeyboard again later, we need to save its state now. The + // later call will be done in #retryResetCaches. switcher.saveKeyboardState(); } } else if (restarting) { @@ -954,6 +982,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void onFinishInputViewInternal(final boolean finishingInput) { super.onFinishInputView(finishingInput); + cleanupInternalStateForFinishInput(); + } + + private void cleanupInternalStateForFinishInput() { mKeyboardSwitcher.deallocateMemory(); // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestionStrip(); @@ -973,13 +1005,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen + ", cs=" + composingSpanStart + ", ce=" + composingSpanEnd); } - // If the keyboard is not visible, we don't need to do all the housekeeping work, as it - // will be reset when the keyboard shows up anyway. - // TODO: revisit this when LatinIME supports hardware keyboards. - // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown(). - // TODO: find a better way to simulate actual execution. - if (isInputViewShown() && - mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd)) { + // This call happens when we have a hardware keyboard as well as when we don't. While we + // don't support hardware keyboards yet we should avoid doing the processing associated + // with cursor movement when we have a hardware keyboard since we are not in charge. + final SettingsValues settingsValues = mSettings.getCurrent(); + if ((!settingsValues.mHasHardwareKeyboard || ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED) + && mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd)) { mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } @@ -1075,12 +1106,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onComputeInsets(final InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); + final SettingsValues settingsValues = mSettings.getCurrent(); final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); if (visibleKeyboardView == null || !hasSuggestionStripView()) { return; } final int inputHeight = mInputView.getHeight(); - final boolean hasHardwareKeyboard = mKeyboardSwitcher.hasHardwareKeyboard(); + final boolean hasHardwareKeyboard = settingsValues.mHasHardwareKeyboard; if (hasHardwareKeyboard && visibleKeyboardView.getVisibility() == View.GONE) { // If there is a hardware keyboard and a visible software keyboard view has been hidden, // no visual element will be shown on the screen. @@ -1116,7 +1148,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public boolean onShowInputRequested(final int flags, final boolean configChange) { - if ((flags & InputMethod.SHOW_EXPLICIT) == 0 && mKeyboardSwitcher.hasHardwareKeyboard()) { + final SettingsValues settingsValues = mSettings.getCurrent(); + if ((flags & InputMethod.SHOW_EXPLICIT) == 0 && settingsValues.mHasHardwareKeyboard) { // Even when IME is implicitly shown and physical keyboard is connected, we should // show {@link InputView}. // See {@link InputMethodService#onShowInputRequested(int,boolean)}. @@ -1127,7 +1160,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public boolean onEvaluateFullscreenMode() { - if (mKeyboardSwitcher.hasHardwareKeyboard()) { + final SettingsValues settingsValues = mSettings.getCurrent(); + if (settingsValues.mHasHardwareKeyboard) { // If there is a hardware keyboard, disable full screen mode. return false; } diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 6b6384c48..dc00ecc8f 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -623,14 +623,24 @@ public final class RichInputConnection { return Arrays.binarySearch(sortedSeparators, code) >= 0; } + private static boolean isPartOfCompositionForScript(final int codePoint, + final SpacingAndPunctuations spacingAndPunctuations, final int scriptId) { + // We always consider word connectors part of compositions. + return spacingAndPunctuations.isWordConnector(codePoint) + // Otherwise, it's part of composition if it's part of script and not a separator. + || (!spacingAndPunctuations.isWordSeparator(codePoint) + && ScriptUtils.isLetterPartOfScript(codePoint, scriptId)); + } + /** * Returns the text surrounding the cursor. * - * @param sortedSeparators a sorted array of code points that split words. + * @param spacingAndPunctuations the rules for spacing and punctuation * @param scriptId the script we consider to be writing words, as one of ScriptUtils.SCRIPT_* * @return a range containing the text surrounding the cursor */ - public TextRange getWordRangeAtCursor(final int[] sortedSeparators, final int scriptId) { + public TextRange getWordRangeAtCursor(final SpacingAndPunctuations spacingAndPunctuations, + final int scriptId) { mIC = mParent.getCurrentInputConnection(); if (mIC == null) { return null; @@ -647,8 +657,7 @@ public final class RichInputConnection { int startIndexInBefore = before.length(); while (startIndexInBefore > 0) { final int codePoint = Character.codePointBefore(before, startIndexInBefore); - if (isSeparator(codePoint, sortedSeparators) - || !ScriptUtils.isLetterPartOfScript(codePoint, scriptId)) { + if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, scriptId)) { break; } --startIndexInBefore; @@ -661,8 +670,7 @@ public final class RichInputConnection { int endIndexInAfter = -1; while (++endIndexInAfter < after.length()) { final int codePoint = Character.codePointAt(after, endIndexInAfter); - if (isSeparator(codePoint, sortedSeparators) - || !ScriptUtils.isLetterPartOfScript(codePoint, scriptId)) { + if (!isPartOfCompositionForScript(codePoint, spacingAndPunctuations, scriptId)) { break; } if (Character.isSupplementaryCodePoint(codePoint)) { @@ -729,17 +737,19 @@ public final class RichInputConnection { return TextUtils.equals(text, beforeText); } - public boolean revertDoubleSpacePeriod() { + public boolean revertDoubleSpacePeriod(final SpacingAndPunctuations spacingAndPunctuations) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); // Here we test whether we indeed have a period and a space before us. This should not // be needed, but it's there just in case something went wrong. final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0); - if (!TextUtils.equals(Constants.STRING_PERIOD_AND_SPACE, textBeforeCursor)) { + if (!TextUtils.equals(spacingAndPunctuations.mSentenceSeparatorAndSpace, + textBeforeCursor)) { // Theoretically we should not be coming here if there isn't ". " before the // cursor, but the application may be changing the text while we are typing, so // anything goes. We should not crash. - Log.d(TAG, "Tried to revert double-space combo but we didn't find " - + "\"" + Constants.STRING_PERIOD_AND_SPACE + "\" just before the cursor."); + Log.d(TAG, "Tried to revert double-space combo but we didn't find \"" + + spacingAndPunctuations.mSentenceSeparatorAndSpace + + "\" just before the cursor."); return false; } // Double-space results in ". ". A backspace to cancel this should result in a single diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 6bc3da885..dcfaa3f6d 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -142,6 +142,15 @@ public class SuggestedWords { return mSuggestedWordInfoList.get(index); } + /** + * Gets the suggestion index from the suggestions list. + * @param suggestedWordInfo The {@link SuggestedWordInfo} to find the index. + * @return The position of the suggestion in the suggestion list. + */ + public int indexOf(SuggestedWordInfo suggestedWordInfo) { + return mSuggestedWordInfoList.indexOf(suggestedWordInfo); + } + public String getDebugString(final int pos) { if (!DebugFlags.DEBUG_ENABLED) { return null; diff --git a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java index 7071d8689..a87785b1a 100644 --- a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java +++ b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java @@ -168,7 +168,7 @@ public class ExternalDictionaryGetterForDebug { } catch (IOException e) { // There was an error: show a dialog new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) - .setTitle(R.string.error) + .setTitle(R.string.read_external_dictionary_error) .setMessage(e.toString()) .setPositiveButton(android.R.string.ok, new OnClickListener() { @Override diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index 233a22512..26acabdaf 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -60,6 +60,7 @@ import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; import com.android.inputmethod.latin.utils.AsyncResultHolder; import com.android.inputmethod.latin.utils.InputTypeUtils; import com.android.inputmethod.latin.utils.RecapitalizeStatus; +import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.StringUtils; import com.android.inputmethod.latin.utils.TextRange; @@ -361,6 +362,8 @@ public final class InputLogic { if (shouldShowAddToDictionaryIndicator) { mTextDecorator.showAddToDictionaryIndicator(suggestionInfo); } + + StatsUtils.onPickSuggestionManually(mSuggestedWords, suggestionInfo); return inputTransaction; } @@ -1112,7 +1115,8 @@ public final class InputLogic { } if (SpaceState.DOUBLE == inputTransaction.mSpaceState) { cancelDoubleSpacePeriodCountdown(); - if (mConnection.revertDoubleSpacePeriod()) { + if (mConnection.revertDoubleSpacePeriod( + inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { // No need to reset mSpaceState, it has already be done (that's why we // receive it as a parameter) inputTransaction.setRequiresUpdateSuggestions(); @@ -1295,7 +1299,9 @@ public final class InputLogic { if (null == lastTwo) return false; final int length = lastTwo.length(); if (length < 2) return false; - if (lastTwo.charAt(length - 1) != Constants.CODE_SPACE) return false; + if (lastTwo.charAt(length - 1) != Constants.CODE_SPACE) { + return false; + } // We know there is a space in pos -1, and we have at least two chars. If we have only two // chars, isSurrogatePairs can't return true as charAt(1) is a space, so this is fine. final int firstCodePoint = @@ -1478,8 +1484,7 @@ public final class InputLogic { return; } final TextRange range = mConnection.getWordRangeAtCursor( - settingsValues.mSpacingAndPunctuations.mSortedWordSeparators, - currentKeyboardScriptId); + settingsValues.mSpacingAndPunctuations, currentKeyboardScriptId); if (null == range) return; // Happens if we don't have an input connection at all if (range.length() <= 0) { // Race condition, or touching a word in a non-supported script. diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index 9d3c27bbe..3c7a99102 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -19,6 +19,7 @@ package com.android.inputmethod.latin.settings; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; +import android.content.res.Configuration; import android.content.res.Resources; import android.os.Build; import android.preference.PreferenceManager; @@ -368,6 +369,15 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return prefs.getBoolean(PREF_SHOW_SETUP_WIZARD_ICON, false); } + public static boolean readHasHardwareKeyboard(final Configuration conf) { + // The standard way of finding out whether we have a hardware keyboard. This code is taken + // from InputMethodService#onEvaluateInputShown, which canonically determines this. + // In a nutshell, we have a keyboard if the configuration says the type of hardware keyboard + // is NOKEYS and if it's not hidden (e.g. folded inside the device). + return conf.keyboard != Configuration.KEYBOARD_NOKEYS + && conf.hardKeyboardHidden != Configuration.HARDKEYBOARDHIDDEN_YES; + } + public static boolean isInternal(final SharedPreferences prefs) { return prefs.getBoolean(PREF_KEY_IS_INTERNAL, false); } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index 11291959c..c891a2e14 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -53,7 +53,10 @@ public class SettingsValues { public final SpacingAndPunctuations mSpacingAndPunctuations; public final int mDelayInMillisecondsToUpdateOldSuggestions; public final long mDoubleSpacePeriodTimeout; - + // From configuration: + public final Locale mLocale; + public final boolean mHasHardwareKeyboard; + public final int mDisplayOrientation; // From preferences, in the same order as xml/prefs.xml: public final boolean mAutoCap; public final boolean mVibrateOn; @@ -74,7 +77,6 @@ public class SettingsValues { public final boolean mSlidingKeyInputPreviewEnabled; public final boolean mPhraseGestureEnabled; public final int mKeyLongpressTimeout; - public final Locale mLocale; public final boolean mEnableMetricsLogging; public final boolean mShouldShowUiToAcceptTypedWord; @@ -89,7 +91,6 @@ public class SettingsValues { public final float mAutoCorrectionThreshold; public final boolean mAutoCorrectionEnabledPerUserSettings; private final boolean mSuggestionsEnabledPerUserSettings; - public final int mDisplayOrientation; private final AsyncResultHolder<AppWorkaroundsUtils> mAppWorkarounds; // Setting values for additional features @@ -153,6 +154,7 @@ public class SettingsValues { mAutoCorrectEnabled = Settings.readAutoCorrectEnabled(autoCorrectionThresholdRawValue, res); mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res); mDoubleSpacePeriodTimeout = res.getInteger(R.integer.config_double_space_period_timeout); + mHasHardwareKeyboard = Settings.readHasHardwareKeyboard(res.getConfiguration()); mEnableMetricsLogging = prefs.getBoolean(Settings.PREF_ENABLE_METRICS_LOGGING, true); mShouldShowUiToAcceptTypedWord = Settings.HAS_UI_TO_ACCEPT_TYPED_WORD && prefs.getBoolean(DebugSettings.PREF_SHOW_UI_TO_ACCEPT_TYPED_WORD, true); diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java index b8d2a2248..97aad3b6d 100644 --- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java +++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin.settings; import android.content.res.Resources; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.internal.MoreKeySpec; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.PunctuationSuggestions; @@ -35,6 +36,8 @@ public final class SpacingAndPunctuations { public final int[] mSortedWordSeparators; public final PunctuationSuggestions mSuggestPuncList; private final int mSentenceSeparator; + private final int mAbbreviationMarker; + private final int[] mSortedSentenceTerminators; public final String mSentenceSeparatorAndSpace; public final boolean mCurrentLanguageHasSpaces; public final boolean mUsesAmericanTypography; @@ -54,7 +57,10 @@ public final class SpacingAndPunctuations { res.getString(R.string.symbols_word_connectors)); mSortedWordSeparators = StringUtils.toSortedCodePointArray( res.getString(R.string.symbols_word_separators)); + mSortedSentenceTerminators = StringUtils.toSortedCodePointArray( + res.getString(R.string.symbols_sentence_terminators)); mSentenceSeparator = res.getInteger(R.integer.sentence_separator); + mAbbreviationMarker = res.getInteger(R.integer.abbreviation_marker); mSentenceSeparatorAndSpace = new String(new int[] { mSentenceSeparator, Constants.CODE_SPACE }, 0, 2); mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces); @@ -68,6 +74,24 @@ public final class SpacingAndPunctuations { mSuggestPuncList = PunctuationSuggestions.newPunctuationSuggestions(suggestPuncsSpec); } + @UsedForTesting + public SpacingAndPunctuations(final SpacingAndPunctuations model, + final int[] overrideSortedWordSeparators) { + mSortedSymbolsPrecededBySpace = model.mSortedSymbolsPrecededBySpace; + mSortedSymbolsFollowedBySpace = model.mSortedSymbolsFollowedBySpace; + mSortedSymbolsClusteringTogether = model.mSortedSymbolsClusteringTogether; + mSortedWordConnectors = model.mSortedWordConnectors; + mSortedWordSeparators = overrideSortedWordSeparators; + mSortedSentenceTerminators = model.mSortedSentenceTerminators; + mSuggestPuncList = model.mSuggestPuncList; + mSentenceSeparator = model.mSentenceSeparator; + mAbbreviationMarker = model.mAbbreviationMarker; + mSentenceSeparatorAndSpace = model.mSentenceSeparatorAndSpace; + mCurrentLanguageHasSpaces = model.mCurrentLanguageHasSpaces; + mUsesAmericanTypography = model.mUsesAmericanTypography; + mUsesGermanRules = model.mUsesGermanRules; + } + public boolean isWordSeparator(final int code) { return Arrays.binarySearch(mSortedWordSeparators, code) >= 0; } @@ -92,6 +116,14 @@ public final class SpacingAndPunctuations { return Arrays.binarySearch(mSortedSymbolsClusteringTogether, code) >= 0; } + public boolean isSentenceTerminator(final int code) { + return Arrays.binarySearch(mSortedSentenceTerminators, code) >= 0; + } + + public boolean isAbbreviationMarker(final int code) { + return code == mAbbreviationMarker; + } + public boolean isSentenceSeparator(final int code) { return code == mSentenceSeparator; } diff --git a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java index 936219332..02f1c5f00 100644 --- a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java @@ -213,12 +213,22 @@ public final class CapsModeUtils { char c = cs.charAt(--j); // We found the next interesting chunk of text ; next we need to determine if it's the - // end of a sentence. If we have a question mark or an exclamation mark, it's the end of - // a sentence. If it's neither, the only remaining case is the period so we get the opposite - // case out of the way. - if (c == Constants.CODE_QUESTION_MARK || c == Constants.CODE_EXCLAMATION_MARK) { - return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes; + // end of a sentence. If we have a sentence terminator (typically a question mark or an + // exclamation mark), then it's the end of a sentence; however, we treat the abbreviation + // marker specially because usually is the same char as the sentence separator (the + // period in most languages) and in this case we need to apply a heuristic to determine + // in which of these senses it's used. + if (spacingAndPunctuations.isSentenceTerminator(c) + && !spacingAndPunctuations.isAbbreviationMarker(c)) { + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS + | TextUtils.CAP_MODE_SENTENCES) & reqModes; } + // If we reach here, we know we have whitespace before the cursor and before that there + // is something that either does not terminate the sentence, or a symbol preceded by the + // start of the text, or it's the sentence separator AND it happens to be the same code + // point as the abbreviation marker. + // If it's a symbol or something that does not terminate the sentence, then we need to + // return caps for MODE_CHARACTERS and MODE_WORDS, but not for MODE_SENTENCES. if (!spacingAndPunctuations.isSentenceSeparator(c) || j <= 0) { return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; } |