diff options
Diffstat (limited to 'java/src')
125 files changed, 5895 insertions, 2616 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java index 7a3510ee1..edcdd4c4c 100644 --- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java +++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java @@ -37,6 +37,8 @@ final class KeyCodeDescriptionMapper { private static final String SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X"; private static final String SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X"; private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X"; + private static final String SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX = "spoken_emoticon"; + private static final String SPOKEN_EMOTICON_CODE_POINT_FORMAT = "_%02X"; // The resource ID of the string spoken for obscured keys private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot; @@ -109,7 +111,9 @@ final class KeyCodeDescriptionMapper { } if (code == Constants.CODE_OUTPUT_TEXT) { - return key.getOutputText(); + final String outputText = key.getOutputText(); + final String description = getSpokenEmoticonDescription(context, outputText); + return TextUtils.isEmpty(description) ? outputText : description; } // Just attempt to speak the description. @@ -340,4 +344,21 @@ final class KeyCodeDescriptionMapper { } return resId; } + + // TODO: Remove this method once TTS supports emoticon verbalization. + private String getSpokenEmoticonDescription(final Context context, final String outputText) { + final StringBuilder sb = new StringBuilder(SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX); + final int textLength = outputText.length(); + for (int index = 0; index < textLength; index = outputText.offsetByCodePoints(index, 1)) { + final int codePoint = outputText.codePointAt(index); + sb.append(String.format(Locale.ROOT, SPOKEN_EMOTICON_CODE_POINT_FORMAT, codePoint)); + } + final String resourceName = sb.toString(); + final Resources resources = context.getResources(); + // Note that the resource package name may differ from the context package name. + final String resourcePackageName = resources.getResourcePackageName( + R.string.spoken_description_unknown); + final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName); + return (resId == 0) ? null : resources.getString(resId); + } } diff --git a/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java index b84d402fb..94a1ee6eb 100644 --- a/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java +++ b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java @@ -121,7 +121,7 @@ public final class MainKeyboardAccessibilityDelegate */ private void announceKeyboardLanguage(final Keyboard keyboard) { final String languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale( - keyboard.mId.mSubtype); + keyboard.mId.mSubtype.getRawSubtype()); sendWindowStateChanged(languageText); } diff --git a/java/src/com/android/inputmethod/compat/CharacterCompat.java b/java/src/com/android/inputmethod/compat/CharacterCompat.java new file mode 100644 index 000000000..609fe1638 --- /dev/null +++ b/java/src/com/android/inputmethod/compat/CharacterCompat.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2014 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 java.lang.reflect.Method; + +public final class CharacterCompat { + // Note that Character.isAlphabetic(int), has been introduced in API level 19 + // (Build.VERSION_CODE.KITKAT). + private static final Method METHOD_isAlphabetic = CompatUtils.getMethod( + Character.class, "isAlphabetic", int.class); + + private CharacterCompat() { + // This utility class is not publicly instantiable. + } + + public static boolean isAlphabetic(final int code) { + if (METHOD_isAlphabetic != null) { + return (Boolean)CompatUtils.invoke(null, false, METHOD_isAlphabetic, code); + } + switch (Character.getType(code)) { + case Character.UPPERCASE_LETTER: + case Character.LOWERCASE_LETTER: + case Character.TITLECASE_LETTER: + case Character.MODIFIER_LETTER: + case Character.OTHER_LETTER: + case Character.LETTER_NUMBER: + return true; + default: + return false; + } + } +} diff --git a/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java b/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java index 5af31795c..f4f54b624 100644 --- a/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java +++ b/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java @@ -16,13 +16,20 @@ package com.android.inputmethod.compat; +import android.annotation.TargetApi; import android.graphics.Matrix; import android.graphics.RectF; +import android.os.Build; +import android.view.inputmethod.CursorAnchorInfo; -import com.android.inputmethod.annotations.UsedForTesting; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; -@UsedForTesting -public final class CursorAnchorInfoCompatWrapper { +/** + * A wrapper for {@link CursorAnchorInfo}, which has been introduced in API Level 21. You can use + * this wrapper to avoid direct dependency on newly introduced types. + */ +public class CursorAnchorInfoCompatWrapper { /** * The insertion marker or character bounds have at least one visible region. @@ -39,123 +46,138 @@ public final class CursorAnchorInfoCompatWrapper { */ 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.ToIntMethodWrapper sGetSelectionStartMethod; - private static final CompatUtils.ToIntMethodWrapper sGetSelectionEndMethod; - 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; - private static final CompatUtils.ToFloatMethodWrapper sGetInsertionMarkerBottomMethod; - private static final CompatUtils.ToFloatMethodWrapper sGetInsertionMarkerHorizontalMethod; - private static final CompatUtils.ToFloatMethodWrapper sGetInsertionMarkerTopMethod; - private static final CompatUtils.ToObjectMethodWrapper<Matrix> sGetMatrixMethod; - private static final CompatUtils.ToIntMethodWrapper sGetInsertionMarkerFlagsMethod; - - private static int INVALID_TEXT_INDEX = -1; - static { - sCursorAnchorInfoClass = CompatUtils.getClassWrapper( - "android.view.inputmethod.CursorAnchorInfo"); - sGetSelectionStartMethod = sCursorAnchorInfoClass.getPrimitiveMethod( - "getSelectionStart", INVALID_TEXT_INDEX); - sGetSelectionEndMethod = sCursorAnchorInfoClass.getPrimitiveMethod( - "getSelectionEnd", INVALID_TEXT_INDEX); - sGetCharacterBoundsMethod = sCursorAnchorInfoClass.getMethod( - "getCharacterBounds", (RectF)null, int.class); - sGetCharacterBoundsFlagsMethod = sCursorAnchorInfoClass.getPrimitiveMethod( - "getCharacterBoundsFlags", 0, int.class); - sGetComposingTextMethod = sCursorAnchorInfoClass.getMethod( - "getComposingText", (CharSequence)null); - sGetComposingTextStartMethod = sCursorAnchorInfoClass.getPrimitiveMethod( - "getComposingTextStart", INVALID_TEXT_INDEX); - sGetInsertionMarkerBaselineMethod = sCursorAnchorInfoClass.getPrimitiveMethod( - "getInsertionMarkerBaseline", 0.0f); - sGetInsertionMarkerBottomMethod = sCursorAnchorInfoClass.getPrimitiveMethod( - "getInsertionMarkerBottom", 0.0f); - sGetInsertionMarkerHorizontalMethod = sCursorAnchorInfoClass.getPrimitiveMethod( - "getInsertionMarkerHorizontal", 0.0f); - sGetInsertionMarkerTopMethod = sCursorAnchorInfoClass.getPrimitiveMethod( - "getInsertionMarkerTop", 0.0f); - sGetMatrixMethod = sCursorAnchorInfoClass.getMethod("getMatrix", (Matrix)null); - sGetInsertionMarkerFlagsMethod = sCursorAnchorInfoClass.getPrimitiveMethod( - "getInsertionMarkerFlags", 0); - } - - @UsedForTesting - public boolean isAvailable() { - return sCursorAnchorInfoClass.exists() && mInstance != null; - } - - private Object mInstance; - - private CursorAnchorInfoCompatWrapper(final Object instance) { - mInstance = instance; + private CursorAnchorInfoCompatWrapper() { + // This class is not publicly instantiable. } - @UsedForTesting - public static CursorAnchorInfoCompatWrapper fromObject(final Object instance) { - if (!sCursorAnchorInfoClass.exists()) { - return new CursorAnchorInfoCompatWrapper(null); + @TargetApi(BuildCompatUtils.VERSION_CODES_LXX) + @Nullable + public static CursorAnchorInfoCompatWrapper wrap(@Nullable final CursorAnchorInfo instance) { + if (Build.VERSION.SDK_INT < BuildCompatUtils.VERSION_CODES_LXX) { + return null; } - return new CursorAnchorInfoCompatWrapper(instance); - } - - private static final class FakeHolder { - static CursorAnchorInfoCompatWrapper sInstance = new CursorAnchorInfoCompatWrapper(null); - } - - @UsedForTesting - public static CursorAnchorInfoCompatWrapper getFake() { - return FakeHolder.sInstance; + if (instance == null) { + return null; + } + return new RealWrapper(instance); } public int getSelectionStart() { - return sGetSelectionStartMethod.invoke(mInstance); + throw new UnsupportedOperationException("not supported."); } public int getSelectionEnd() { - return sGetSelectionEndMethod.invoke(mInstance); + throw new UnsupportedOperationException("not supported."); } public CharSequence getComposingText() { - return sGetComposingTextMethod.invoke(mInstance); + throw new UnsupportedOperationException("not supported."); } public int getComposingTextStart() { - return sGetComposingTextStartMethod.invoke(mInstance); + throw new UnsupportedOperationException("not supported."); } public Matrix getMatrix() { - return sGetMatrixMethod.invoke(mInstance); + throw new UnsupportedOperationException("not supported."); } public RectF getCharacterBounds(final int index) { - return sGetCharacterBoundsMethod.invoke(mInstance, index); + throw new UnsupportedOperationException("not supported."); } public int getCharacterBoundsFlags(final int index) { - return sGetCharacterBoundsFlagsMethod.invoke(mInstance, index); + throw new UnsupportedOperationException("not supported."); } public float getInsertionMarkerBaseline() { - return sGetInsertionMarkerBaselineMethod.invoke(mInstance); + throw new UnsupportedOperationException("not supported."); } public float getInsertionMarkerBottom() { - return sGetInsertionMarkerBottomMethod.invoke(mInstance); + throw new UnsupportedOperationException("not supported."); } public float getInsertionMarkerHorizontal() { - return sGetInsertionMarkerHorizontalMethod.invoke(mInstance); + throw new UnsupportedOperationException("not supported."); } public float getInsertionMarkerTop() { - return sGetInsertionMarkerTopMethod.invoke(mInstance); + throw new UnsupportedOperationException("not supported."); } public int getInsertionMarkerFlags() { - return sGetInsertionMarkerFlagsMethod.invoke(mInstance); + throw new UnsupportedOperationException("not supported."); + } + + @TargetApi(BuildCompatUtils.VERSION_CODES_LXX) + private static final class RealWrapper extends CursorAnchorInfoCompatWrapper { + + @Nonnull + private final CursorAnchorInfo mInstance; + + public RealWrapper(@Nonnull final CursorAnchorInfo info) { + mInstance = info; + } + + @Override + public int getSelectionStart() { + return mInstance.getSelectionStart(); + } + + @Override + public int getSelectionEnd() { + return mInstance.getSelectionEnd(); + } + + @Override + public CharSequence getComposingText() { + return mInstance.getComposingText(); + } + + @Override + public int getComposingTextStart() { + return mInstance.getComposingTextStart(); + } + + @Override + public Matrix getMatrix() { + return mInstance.getMatrix(); + } + + @Override + public RectF getCharacterBounds(final int index) { + return mInstance.getCharacterBounds(index); + } + + @Override + public int getCharacterBoundsFlags(final int index) { + return mInstance.getCharacterBoundsFlags(index); + } + + @Override + public float getInsertionMarkerBaseline() { + return mInstance.getInsertionMarkerBaseline(); + } + + @Override + public float getInsertionMarkerBottom() { + return mInstance.getInsertionMarkerBottom(); + } + + @Override + public float getInsertionMarkerHorizontal() { + return mInstance.getInsertionMarkerHorizontal(); + } + + @Override + public float getInsertionMarkerTop() { + return mInstance.getInsertionMarkerTop(); + } + + @Override + public int getInsertionMarkerFlags() { + return mInstance.getInsertionMarkerFlags(); + } } } diff --git a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java index 365867257..b9a536721 100644 --- a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java @@ -21,6 +21,7 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.RichInputMethodSubtype; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -64,6 +65,10 @@ public final class InputMethodSubtypeCompatUtils { overridesImplicitlyEnabledSubtype, id); } + public static boolean isAsciiCapable(final RichInputMethodSubtype subtype) { + return isAsciiCapable(subtype.getRawSubtype()); + } + public static boolean isAsciiCapable(final InputMethodSubtype subtype) { return isAsciiCapableWithAPI(subtype) || subtype.containsExtraValueKey(Constants.Subtype.ExtraValue.ASCII_CAPABLE); diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java index c33c01552..d3e24e342 100644 --- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java +++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java @@ -23,13 +23,18 @@ import android.text.Spanned; import android.text.TextUtils; import android.text.style.SuggestionSpan; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.define.DebugFlags; import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver; +import com.android.inputmethod.latin.define.DebugFlags; +import com.android.inputmethod.latin.utils.LocaleUtils; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Locale; + +import javax.annotation.Nullable; public final class SuggestionSpanUtils { // Note that SuggestionSpan.FLAG_AUTO_CORRECTION has been introduced @@ -51,12 +56,14 @@ public final class SuggestionSpanUtils { // This utility class is not publicly instantiable. } + @UsedForTesting public static CharSequence getTextWithAutoCorrectionIndicatorUnderline( final Context context, final String text) { if (TextUtils.isEmpty(text) || OBJ_FLAG_AUTO_CORRECTION == null) { return text; } final Spannable spannable = new SpannableString(text); + // TODO: Set locale if it is feasible. final SuggestionSpan suggestionSpan = new SuggestionSpan(context, null /* locale */, new String[] {} /* suggestions */, OBJ_FLAG_AUTO_CORRECTION, SuggestionSpanPickedNotificationReceiver.class); @@ -65,6 +72,7 @@ public final class SuggestionSpanUtils { return spannable; } + @UsedForTesting public static CharSequence getTextWithSuggestionSpan(final Context context, final String pickedWord, final SuggestedWords suggestedWords) { if (TextUtils.isEmpty(pickedWord) || suggestedWords.isEmpty() @@ -86,6 +94,7 @@ public final class SuggestionSpanUtils { suggestionsList.add(word.toString()); } } + // TODO: Set locale if it is feasible. final SuggestionSpan suggestionSpan = new SuggestionSpan(context, null /* locale */, suggestionsList.toArray(new String[suggestionsList.size()]), 0 /* flags */, SuggestionSpanPickedNotificationReceiver.class); @@ -93,4 +102,28 @@ public final class SuggestionSpanUtils { spannable.setSpan(suggestionSpan, 0, pickedWord.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return spannable; } + + /** + * Returns first {@link Locale} found in the given array of {@link SuggestionSpan}. + * @param suggestionSpans the array of {@link SuggestionSpan} to be examined. + * @return the first {@link Locale} found in {@code suggestionSpans}. {@code null} when not + * found. + */ + @UsedForTesting + @Nullable + public static Locale findFirstLocaleFromSuggestionSpans( + final SuggestionSpan[] suggestionSpans) { + for (final SuggestionSpan suggestionSpan : suggestionSpans) { + final String localeString = suggestionSpan.getLocale(); + if (TextUtils.isEmpty(localeString)) { + continue; + } + final Locale locale = LocaleUtils.constructLocaleFromString(localeString); + if (locale == null) { + continue; + } + return locale; + } + return null; + } } diff --git a/java/src/com/android/inputmethod/compat/ViewCompatUtils.java b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java index 0f00be133..16260ab6a 100644 --- a/java/src/com/android/inputmethod/compat/ViewCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java @@ -31,9 +31,6 @@ public final class ViewCompatUtils { private static final Method METHOD_setPaddingRelative = CompatUtils.getMethod( View.class, "setPaddingRelative", int.class, int.class, int.class, int.class); - // Note that View.setElevation(float) has been introduced in API level 21. - private static final Method METHOD_setElevation = CompatUtils.getMethod( - View.class, "setElevation", float.class); // Note that View.setTextAlignment(int) has been introduced in API level 17. private static final Method METHOD_setTextAlignment = CompatUtils.getMethod( View.class, "setTextAlignment", int.class); @@ -58,10 +55,6 @@ public final class ViewCompatUtils { CompatUtils.invoke(view, null, METHOD_setPaddingRelative, start, top, end, bottom); } - public static void setElevation(final View view, final float elevation) { - CompatUtils.invoke(view, null, METHOD_setElevation, elevation); - } - // These TEXT_ALIGNMENT_* constants have been introduced in API 17. public static final int TEXT_ALIGNMENT_INHERIT = 0; public static final int TEXT_ALIGNMENT_GRAVITY = 1; diff --git a/java/src/com/android/inputmethod/compat/ViewOutlineProviderCompatUtils.java b/java/src/com/android/inputmethod/compat/ViewOutlineProviderCompatUtils.java new file mode 100644 index 000000000..52b8b74e8 --- /dev/null +++ b/java/src/com/android/inputmethod/compat/ViewOutlineProviderCompatUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 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.inputmethodservice.InputMethodService; +import android.view.View; + +public class ViewOutlineProviderCompatUtils { + private ViewOutlineProviderCompatUtils() { + // This utility class is not publicly instantiable. + } + + public interface InsetsUpdater { + public void setInsets(final InputMethodService.Insets insets); + } + + private static final InsetsUpdater EMPTY_INSETS_UPDATER = new InsetsUpdater() { + @Override + public void setInsets(final InputMethodService.Insets insets) {} + }; + + public static InsetsUpdater setInsetsOutlineProvider(final View view) { + if (BuildCompatUtils.EFFECTIVE_SDK_INT < BuildCompatUtils.VERSION_CODES_LXX) { + return EMPTY_INSETS_UPDATER; + } + return ViewOutlineProviderCompatUtilsLXX.setInsetsOutlineProvider(view); + } +} diff --git a/java/src/com/android/inputmethod/compat/ViewOutlineProviderCompatUtilsLXX.java b/java/src/com/android/inputmethod/compat/ViewOutlineProviderCompatUtilsLXX.java new file mode 100644 index 000000000..f9917ac11 --- /dev/null +++ b/java/src/com/android/inputmethod/compat/ViewOutlineProviderCompatUtilsLXX.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2014 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.annotation.TargetApi; +import android.graphics.Outline; +import android.inputmethodservice.InputMethodService; +import android.os.Build; +import android.view.View; +import android.view.ViewOutlineProvider; + +import com.android.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater; + +@TargetApi(Build.VERSION_CODES.L) +class ViewOutlineProviderCompatUtilsLXX { + private ViewOutlineProviderCompatUtilsLXX() { + // This utility class is not publicly instantiable. + } + + static InsetsUpdater setInsetsOutlineProvider(final View view) { + final InsetsOutlineProvider provider = new InsetsOutlineProvider(view); + view.setOutlineProvider(provider); + return provider; + } + + private static class InsetsOutlineProvider extends ViewOutlineProvider + implements InsetsUpdater { + private final View mView; + private static final int NO_DATA = -1; + private int mLastVisibleTopInsets = NO_DATA; + + public InsetsOutlineProvider(final View view) { + mView = view; + view.setOutlineProvider(this); + } + + @Override + public void setInsets(final InputMethodService.Insets insets) { + final int visibleTopInsets = insets.visibleTopInsets; + if (mLastVisibleTopInsets != visibleTopInsets) { + mLastVisibleTopInsets = visibleTopInsets; + mView.invalidateOutline(); + } + } + + @Override + public void getOutline(final View view, final Outline outline) { + if (mLastVisibleTopInsets == NO_DATA) { + // Call default implementation. + ViewOutlineProvider.BACKGROUND.getOutline(view, outline); + return; + } + // TODO: Revisit this when floating/resize keyboard is supported. + outline.setRect( + view.getLeft(), mLastVisibleTopInsets, view.getRight(), view.getBottom()); + } + } +} diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java index 3d294acd7..3aa026e77 100644 --- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java +++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java @@ -120,9 +120,10 @@ public final class ActionBatch { if (MetadataDbHelper.STATUS_DOWNLOADING == status) { // The word list is still downloading. Cancel the download and revert the // word list status to "available". - manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); + manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion); - } else if (MetadataDbHelper.STATUS_AVAILABLE != status) { + } else if (MetadataDbHelper.STATUS_AVAILABLE != status + && MetadataDbHelper.STATUS_RETRYING != status) { // Should never happen Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' : " + status + " for an upgrade action. Fall back to download."); @@ -325,8 +326,8 @@ public final class ActionBatch { mWordList.mId, mWordList.mLocale, mWordList.mDescription, null == mWordList.mLocalFilename ? "" : mWordList.mLocalFilename, mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, - mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, - mWordList.mFormatVersion); + mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize, + mWordList.mVersion, mWordList.mFormatVersion); PrivateLog.log("Insert 'available' record for " + mWordList.mDescription + " and locale " + mWordList.mLocale); db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values); @@ -374,9 +375,9 @@ public final class ActionBatch { final ContentValues values = MetadataDbHelper.makeContentValues(0, MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_INSTALLED, mWordList.mId, mWordList.mLocale, mWordList.mDescription, - "", mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, - mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, - mWordList.mFormatVersion); + "", mWordList.mRemoteFilename, mWordList.mLastUpdate, + mWordList.mRawChecksum, mWordList.mChecksum, mWordList.mRetryCount, + mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion); PrivateLog.log("Insert 'preinstalled' record for " + mWordList.mDescription + " and locale " + mWordList.mLocale); db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values); @@ -417,8 +418,8 @@ public final class ActionBatch { mWordList.mId, mWordList.mLocale, mWordList.mDescription, oldValues.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN), mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum, - mWordList.mChecksum, mWordList.mFileSize, mWordList.mVersion, - mWordList.mFormatVersion); + mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize, + mWordList.mVersion, mWordList.mFormatVersion); PrivateLog.log("Updating record for " + mWordList.mDescription + " and locale " + mWordList.mLocale); db.update(MetadataDbHelper.METADATA_TABLE_NAME, values, diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java index f5bd84c8c..e748321e2 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java @@ -470,7 +470,11 @@ public final class DictionaryProvider extends ContentProvider { } else if (MetadataDbHelper.STATUS_INSTALLED == status) { final String result = uri.getQueryParameter(QUERY_PARAMETER_DELETE_RESULT); if (QUERY_PARAMETER_FAILURE.equals(result)) { - UpdateHandler.markAsBroken(getContext(), clientId, wordlistId, version); + if (DEBUG) { + Log.d(TAG, + "Dictionary is broken, attempting to retry download & installation."); + } + UpdateHandler.markAsBrokenOrRetrying(getContext(), clientId, wordlistId, version); } final String localFilename = wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN); diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java index 41916b614..568c80abd 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java @@ -76,19 +76,29 @@ public final class DictionaryService extends Service { * How often, in milliseconds, we want to update the metadata. This is a * floor value; actually, it may happen several hours later, or even more. */ - private static final long UPDATE_FREQUENCY = TimeUnit.DAYS.toMillis(4); + private static final long UPDATE_FREQUENCY_MILLIS = TimeUnit.DAYS.toMillis(4); /** * We are waked around midnight, local time. We want to wake between midnight and 6 am, * roughly. So use a random time between 0 and this delay. */ - private static final int MAX_ALARM_DELAY = (int)TimeUnit.HOURS.toMillis(6); + private static final int MAX_ALARM_DELAY_MILLIS = (int)TimeUnit.HOURS.toMillis(6); /** * How long we consider a "very long time". If no update took place in this time, * the content provider will trigger an update in the background. */ - private static final long VERY_LONG_TIME = TimeUnit.DAYS.toMillis(14); + private static final long VERY_LONG_TIME_MILLIS = TimeUnit.DAYS.toMillis(14); + + /** + * After starting a download, how long we wait before considering it may be stuck. After this + * period is elapsed, if the keyboard tries to download again, then we cancel and re-register + * the request; if it's within this time, we just leave it be. + * It's important to note that we do not re-submit the request merely because the time is up. + * This is only to decide whether to cancel the old one and re-requesting when the keyboard + * fires a new request for the same data. + */ + public static final long NO_CANCEL_DOWNLOAD_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(30); /** * An executor that serializes tasks given to it. @@ -188,16 +198,16 @@ public final class DictionaryService extends Service { */ private static void checkTimeAndMaybeSetupUpdateAlarm(final Context context) { // Of all clients, if the one that hasn't been updated for the longest - // is still more recent than UPDATE_FREQUENCY, do nothing. - if (!isLastUpdateAtLeastThisOld(context, UPDATE_FREQUENCY)) return; + // is still more recent than UPDATE_FREQUENCY_MILLIS, do nothing. + if (!isLastUpdateAtLeastThisOld(context, UPDATE_FREQUENCY_MILLIS)) return; PrivateLog.log("Date changed - registering alarm"); AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); - // Best effort to wake between midnight and MAX_ALARM_DELAY in the morning. + // Best effort to wake between midnight and MAX_ALARM_DELAY_MILLIS in the morning. // It doesn't matter too much if this is very inexact. final long now = System.currentTimeMillis(); - final long alarmTime = now + new Random().nextInt(MAX_ALARM_DELAY); + final long alarmTime = now + new Random().nextInt(MAX_ALARM_DELAY_MILLIS); final Intent updateIntent = new Intent(DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION); final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, updateIntent, PendingIntent.FLAG_CANCEL_CURRENT); @@ -223,11 +233,11 @@ public final class DictionaryService extends Service { /** * Refreshes data if it hasn't been refreshed in a very long time. * - * This will check the last update time, and if it's been more than VERY_LONG_TIME, + * This will check the last update time, and if it's been more than VERY_LONG_TIME_MILLIS, * update metadata now - and possibly take subsequent update actions. */ public static void updateNowIfNotUpdatedInAVeryLongTime(final Context context) { - if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME)) return; + if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME_MILLIS)) return; UpdateHandler.tryUpdate(context, false); } diff --git a/java/src/com/android/inputmethod/dictionarypack/DownloadIdAndStartDate.java b/java/src/com/android/inputmethod/dictionarypack/DownloadIdAndStartDate.java new file mode 100644 index 000000000..6247a15e2 --- /dev/null +++ b/java/src/com/android/inputmethod/dictionarypack/DownloadIdAndStartDate.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014 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.dictionarypack; + +/** + * A simple container of download ID and download start date. + */ +public class DownloadIdAndStartDate { + public final long mId; + public final long mStartDate; + public DownloadIdAndStartDate(final long id, final long startDate) { + mId = id; + mStartDate = startDate; + } +} diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java index 17dd781d5..c9e8f9118 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java @@ -47,10 +47,13 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // used to identify the versions for upgrades. This should never change going forward. private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6; // The current database version. - private static final int CURRENT_METADATA_DATABASE_VERSION = 9; + private static final int CURRENT_METADATA_DATABASE_VERSION = 10; private final static long NOT_A_DOWNLOAD_ID = -1; + // The number of retries allowed when attempting to download a broken dictionary. + public static final int DICTIONARY_RETRY_THRESHOLD = 2; + public static final String METADATA_TABLE_NAME = "pendingUpdates"; static final String CLIENT_TABLE_NAME = "clients"; public static final String PENDINGID_COLUMN = "pendingid"; // Download Manager ID @@ -68,7 +71,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { public static final String FORMATVERSION_COLUMN = "formatversion"; public static final String FLAGS_COLUMN = "flags"; public static final String RAW_CHECKSUM_COLUMN = "rawChecksum"; - public static final int COLUMN_COUNT = 14; + public static final String RETRY_COUNT_COLUMN = "remainingRetries"; + public static final int COLUMN_COUNT = 15; private static final String CLIENT_CLIENT_ID_COLUMN = "clientid"; private static final String CLIENT_METADATA_URI_COLUMN = "uri"; @@ -98,6 +102,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // Deleting: the user marked this word list to be deleted, but it has not been yet because // Latin IME is not up yet. public static final int STATUS_DELETING = 5; + // Retry: dictionary got corrupted, so an attempt must be done to download & install it again. + public static final int STATUS_RETRYING = 6; // Types, for storing in the TYPE_COLUMN // This is metadata about what is available. @@ -124,6 +130,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { + FORMATVERSION_COLUMN + " INTEGER, " + FLAGS_COLUMN + " INTEGER, " + RAW_CHECKSUM_COLUMN + " TEXT," + + RETRY_COUNT_COLUMN + " INTEGER, " + "PRIMARY KEY (" + WORDLISTID_COLUMN + "," + VERSION_COLUMN + "));"; private static final String METADATA_CREATE_CLIENT_TABLE = "CREATE TABLE IF NOT EXISTS " + CLIENT_TABLE_NAME + " (" @@ -140,7 +147,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { STATUS_COLUMN, WORDLISTID_COLUMN, LOCALE_COLUMN, DESCRIPTION_COLUMN, LOCAL_FILENAME_COLUMN, REMOTE_FILENAME_COLUMN, DATE_COLUMN, CHECKSUM_COLUMN, FILESIZE_COLUMN, VERSION_COLUMN, FORMATVERSION_COLUMN, FLAGS_COLUMN, - RAW_CHECKSUM_COLUMN }; + RAW_CHECKSUM_COLUMN, RETRY_COUNT_COLUMN }; // List of all client table columns. static final String[] CLIENT_TABLE_COLUMNS = { CLIENT_CLIENT_ID_COLUMN, CLIENT_METADATA_URI_COLUMN, CLIENT_PENDINGID_COLUMN, FLAGS_COLUMN }; @@ -219,7 +226,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { createClientTable(db); } - private void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db, final String clientId) { + private void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db) { try { db.execSQL("SELECT " + RAW_CHECKSUM_COLUMN + " FROM " + METADATA_TABLE_NAME + " LIMIT 0;"); @@ -230,6 +237,17 @@ public class MetadataDbHelper extends SQLiteOpenHelper { } } + private void addRetryCountColumnUnlessPresent(final SQLiteDatabase db) { + try { + db.execSQL("SELECT " + RETRY_COUNT_COLUMN + " FROM " + + METADATA_TABLE_NAME + " LIMIT 0;"); + } catch (SQLiteException e) { + Log.i(TAG, "No " + RETRY_COUNT_COLUMN + " column : creating it"); + db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN " + + RETRY_COUNT_COLUMN + " INTEGER DEFAULT " + DICTIONARY_RETRY_THRESHOLD + ";"); + } + } + /** * Upgrade the database. Upgrade from version 3 is supported. * Version 3 has a DB named METADATA_DATABASE_NAME_STEM containing a table METADATA_TABLE_NAME. @@ -280,7 +298,14 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // strengthen the system against corrupted dictionary files. // The most secure way to upgrade a database is to just test for the column presence, and // add it if it's not there. - addRawChecksumColumnUnlessPresent(db, mClientId); + addRawChecksumColumnUnlessPresent(db); + + // A retry count column that did not exist in the previous versions was added that + // corresponds to the number of download & installation attempts that have been made + // in order to strengthen the system recovery from corrupted dictionary files. + // The most secure way to upgrade a database is to just test for the column presence, and + // add it if it's not there. + addRetryCountColumnUnlessPresent(db); } /** @@ -408,18 +433,18 @@ public class MetadataDbHelper extends SQLiteOpenHelper { * * @param context a context instance to open the database on * @param uri the URI to retrieve the metadata download ID of - * @return the metadata download ID, or NOT_AN_ID if no download is in progress + * @return the download id and start date, or null if the URL is not known */ - public static long getMetadataDownloadIdForURI(final Context context, - final String uri) { + public static DownloadIdAndStartDate getMetadataDownloadIdAndStartDateForURI( + final Context context, final String uri) { SQLiteDatabase defaultDb = getDb(context, null); final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, - new String[] { CLIENT_PENDINGID_COLUMN }, + new String[] { CLIENT_PENDINGID_COLUMN, CLIENT_LAST_UPDATE_DATE_COLUMN }, CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri }, null, null, null, null); try { - if (!cursor.moveToFirst()) return UpdateHandler.NOT_AN_ID; - return cursor.getInt(0); // Only one column, return it + if (!cursor.moveToFirst()) return null; + return new DownloadIdAndStartDate(cursor.getInt(0), cursor.getLong(1)); } finally { cursor.close(); } @@ -452,8 +477,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { public static ContentValues makeContentValues(final int pendingId, final int type, final int status, final String wordlistId, final String locale, final String description, final String filename, final String url, final long date, - final String rawChecksum, final String checksum, final long filesize, final int version, - final int formatVersion) { + final String rawChecksum, final String checksum, final int retryCount, + final long filesize, final int version, final int formatVersion) { final ContentValues result = new ContentValues(COLUMN_COUNT); result.put(PENDINGID_COLUMN, pendingId); result.put(TYPE_COLUMN, type); @@ -465,6 +490,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { result.put(REMOTE_FILENAME_COLUMN, url); result.put(DATE_COLUMN, date); result.put(RAW_CHECKSUM_COLUMN, rawChecksum); + result.put(RETRY_COUNT_COLUMN, retryCount); result.put(CHECKSUM_COLUMN, checksum); result.put(FILESIZE_COLUMN, filesize); result.put(VERSION_COLUMN, version); @@ -502,6 +528,9 @@ public class MetadataDbHelper extends SQLiteOpenHelper { if (null == result.get(DATE_COLUMN)) result.put(DATE_COLUMN, 0); // Raw checksum unknown unless specified if (null == result.get(RAW_CHECKSUM_COLUMN)) result.put(RAW_CHECKSUM_COLUMN, ""); + // Retry column 0 unless specified + if (null == result.get(RETRY_COUNT_COLUMN)) result.put(RETRY_COUNT_COLUMN, + DICTIONARY_RETRY_THRESHOLD); // Checksum unknown unless specified if (null == result.get(CHECKSUM_COLUMN)) result.put(CHECKSUM_COLUMN, ""); // No filesize unless specified @@ -551,6 +580,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { putIntResult(result, cursor, DATE_COLUMN); putStringResult(result, cursor, RAW_CHECKSUM_COLUMN); putStringResult(result, cursor, CHECKSUM_COLUMN); + putIntResult(result, cursor, RETRY_COUNT_COLUMN); putIntResult(result, cursor, FILESIZE_COLUMN); putIntResult(result, cursor, VERSION_COLUMN); putIntResult(result, cursor, FORMATVERSION_COLUMN); @@ -676,8 +706,16 @@ public class MetadataDbHelper extends SQLiteOpenHelper { final String id, final int version) { final Cursor cursor = db.query(METADATA_TABLE_NAME, METADATA_TABLE_COLUMNS, - WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ?", - new String[] { id, Integer.toString(version) }, null, null, null); + WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ? AND " + + FORMATVERSION_COLUMN + "<= ?", + new String[] + { id, + Integer.toString(version), + Integer.toString(UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION) + }, + null /* groupBy */, + null /* having */, + FORMATVERSION_COLUMN + " DESC"/* orderBy */); if (null == cursor) { return null; } @@ -706,7 +744,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { return null; } try { - // This is a lookup by primary key, so there can't be more than one result. + // Return the first result from the list of results. return getFirstLineAsContentValues(cursor); } finally { cursor.close(); @@ -884,6 +922,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { final long downloadId) { final ContentValues values = new ContentValues(); values.put(CLIENT_PENDINGID_COLUMN, downloadId); + values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis()); final SQLiteDatabase defaultDb = getDb(context, ""); final Cursor cursor = MetadataDbHelper.queryClientIds(context); if (null == cursor) return; @@ -1085,4 +1124,27 @@ public class MetadataDbHelper extends SQLiteOpenHelper { final int version) { markEntryAs(db, id, version, STATUS_DELETING, NOT_A_DOWNLOAD_ID); } + + /** + * Checks retry counts and marks the word list as retrying if retry is possible. + * + * @param db the metadata database. + * @param id the id of the word list. + * @param version the version of the word list. + * @return {@code true} if the retry is possible. + */ + public static boolean maybeMarkEntryAsRetrying(final SQLiteDatabase db, final String id, + final int version) { + final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, id, version); + int retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN); + if (retryCount > 1) { + values.put(STATUS_COLUMN, STATUS_RETRYING); + values.put(RETRY_COUNT_COLUMN, retryCount - 1); + db.update(METADATA_TABLE_NAME, values, + WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?", + new String[] { id, Integer.toString(version) }); + return true; + } + return false; + } } diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java index d66b69050..639d904a0 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java @@ -16,6 +16,7 @@ package com.android.inputmethod.dictionarypack; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -55,6 +56,7 @@ public class MetadataHandler { final int rawChecksumIndex = results.getColumnIndex(MetadataDbHelper.RAW_CHECKSUM_COLUMN); final int checksumIndex = results.getColumnIndex(MetadataDbHelper.CHECKSUM_COLUMN); + final int retryCountIndex = results.getColumnIndex(MetadataDbHelper.RETRY_COUNT_COLUMN); final int localFilenameIndex = results.getColumnIndex(MetadataDbHelper.LOCAL_FILENAME_COLUMN); final int remoteFilenameIndex = @@ -70,6 +72,7 @@ public class MetadataHandler { results.getLong(fileSizeIndex), results.getString(rawChecksumIndex), results.getString(checksumIndex), + results.getInt(retryCountIndex), results.getString(localFilenameIndex), results.getString(remoteFilenameIndex), results.getInt(versionIndex), @@ -102,6 +105,22 @@ public class MetadataHandler { } /** + * Gets the metadata, for a specific dictionary. + * + * @param context The context to open files over. + * @param clientId the client id for retrieving the database. null for default (deprecated). + * @param wordListId the word list ID. + * @param version the word list version. + * @return the current metaData + */ + public static WordListMetadata getCurrentMetadataForWordList(final Context context, + final String clientId, final String wordListId, final int version) { + final ContentValues contentValues = MetadataDbHelper.getContentValuesByWordListId( + MetadataDbHelper.getDb(context, clientId), wordListId, version); + return WordListMetadata.createFromContentValues(contentValues); + } + + /** * Read metadata from a stream. * @param input The stream to read from. * @return The read metadata. diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java b/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java index 52290cadc..2b67ae9ff 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataParser.java @@ -83,6 +83,7 @@ public class MetadataParser { Long.parseLong(arguments.get(FILESIZE_FIELD_NAME)), arguments.get(RAW_CHECKSUM_FIELD_NAME), arguments.get(CHECKSUM_FIELD_NAME), + MetadataDbHelper.DICTIONARY_RETRY_THRESHOLD /* retryCount */, null, arguments.get(REMOTE_FILENAME_FIELD_NAME), Integer.parseInt(arguments.get(VERSION_FIELD_NAME)), diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java index 6fbca44c5..90a750493 100644 --- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java @@ -31,7 +31,6 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.ConnectivityManager; import android.net.Uri; -import android.os.Build; import android.os.ParcelFileDescriptor; import android.text.TextUtils; import android.util.Log; @@ -252,12 +251,16 @@ public final class UpdateHandler { res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI)); final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); - cancelUpdateWithDownloadManager(context, metadataUri, manager); + if (maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager, + DictionaryService.NO_CANCEL_DOWNLOAD_PERIOD_MILLIS)) { + // We already have a recent download in progress. Don't register a new download. + return; + } final long downloadId; synchronized (sSharedIdProtector) { downloadId = manager.enqueue(metadataRequest); DebugLogUtils.l("Metadata download requested with id", downloadId); - // If there is already a download in progress, it's been there for a while and + // If there is still a download in progress, it's been there for a while and // there is probably something wrong with download manager. It's best to just // overwrite the id and request it again. If the old one happens to finish // anyway, we don't know about its ID any more, so the downloadFinished @@ -268,21 +271,29 @@ public final class UpdateHandler { } /** - * Cancels downloading a file, if there is one for this URI. + * Cancels downloading a file if there is one for this URI and it's too long. * * If we are not currently downloading the file at this URI, this is a no-op. * * @param context the context to open the database on * @param metadataUri the URI to cancel * @param manager an wrapped instance of DownloadManager + * @param graceTime if there was a download started less than this many milliseconds, don't + * cancel and return true + * @return whether the download is still active */ - private static void cancelUpdateWithDownloadManager(final Context context, - final String metadataUri, final DownloadManagerWrapper manager) { + private static boolean maybeCancelUpdateAndReturnIfStillRunning(final Context context, + final String metadataUri, final DownloadManagerWrapper manager, final long graceTime) { synchronized (sSharedIdProtector) { - final long metadataDownloadId = - MetadataDbHelper.getMetadataDownloadIdForURI(context, metadataUri); - if (NOT_AN_ID == metadataDownloadId) return; - manager.remove(metadataDownloadId); + final DownloadIdAndStartDate metadataDownloadIdAndStartDate = + MetadataDbHelper.getMetadataDownloadIdAndStartDateForURI(context, metadataUri); + if (null == metadataDownloadIdAndStartDate) return false; + if (NOT_AN_ID == metadataDownloadIdAndStartDate.mId) return false; + if (metadataDownloadIdAndStartDate.mStartDate + graceTime + > System.currentTimeMillis()) { + return true; + } + manager.remove(metadataDownloadIdAndStartDate.mId); writeMetadataDownloadId(context, metadataUri, NOT_AN_ID); } // Consider a cancellation as a failure. As such, inform listeners that the download @@ -290,6 +301,7 @@ public final class UpdateHandler { for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { listener.downloadedMetadata(false); } + return false; } /** @@ -304,7 +316,7 @@ public final class UpdateHandler { public static void cancelUpdate(final Context context, final String clientId) { final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId); - cancelUpdateWithDownloadManager(context, metadataUri, manager); + maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager, 0 /* graceTime */); } /** @@ -388,7 +400,7 @@ public final class UpdateHandler { // If any of these is metadata, we should update the DB boolean hasMetadata = false; for (DownloadRecord record : downloadRecords) { - if (null == record.mAttributes) { + if (record.isMetadata()) { hasMetadata = true; break; } @@ -785,6 +797,10 @@ public final class UpdateHandler { } else { final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); if (newInfo.mVersion == currentInfo.mVersion) { + if (newInfo.mRemoteFilename == currentInfo.mRemoteFilename) { + // If the dictionary url hasn't changed, we should preserve the retryCount. + newInfo.mRetryCount = currentInfo.mRetryCount; + } // If it's the same id/version, we update the DB with the new values. // It doesn't matter too much if they didn't change. actions.add(new ActionBatch.UpdateDataAction(clientId, newInfo)); @@ -987,16 +1003,17 @@ public final class UpdateHandler { public static void markAsUsed(final Context context, final String clientId, final String wordlistId, final int version, final int status, final boolean allowDownloadOnMeteredData) { - final List<WordListMetadata> currentMetadata = - MetadataHandler.getCurrentMetadata(context, clientId); - WordListMetadata wordList = MetadataHandler.findWordListById(currentMetadata, wordlistId); - if (null == wordList) return; + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; + final ActionBatch actions = new ActionBatch(); if (MetadataDbHelper.STATUS_DISABLED == status || MetadataDbHelper.STATUS_DELETING == status) { - actions.add(new ActionBatch.EnableAction(clientId, wordList)); + actions.add(new ActionBatch.EnableAction(clientId, wordListMetaData)); } else if (MetadataDbHelper.STATUS_AVAILABLE == status) { - actions.add(new ActionBatch.StartDownloadAction(clientId, wordList, + actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData, allowDownloadOnMeteredData)); } else { Log.e(TAG, "Unexpected state of the word list for markAsUsed : " + status); @@ -1022,13 +1039,13 @@ public final class UpdateHandler { // markAsUsed for consistency. public static void markAsUnused(final Context context, final String clientId, final String wordlistId, final int version, final int status) { - final List<WordListMetadata> currentMetadata = - MetadataHandler.getCurrentMetadata(context, clientId); - final WordListMetadata wordList = - MetadataHandler.findWordListById(currentMetadata, wordlistId); - if (null == wordList) return; + + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.DisableAction(clientId, wordList)); + actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData)); actions.execute(context, new LogProblemReporter(TAG)); signalNewDictionaryState(context); } @@ -1051,14 +1068,14 @@ public final class UpdateHandler { */ public static void markAsDeleting(final Context context, final String clientId, final String wordlistId, final int version, final int status) { - final List<WordListMetadata> currentMetadata = - MetadataHandler.getCurrentMetadata(context, clientId); - final WordListMetadata wordList = - MetadataHandler.findWordListById(currentMetadata, wordlistId); - if (null == wordList) return; + + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.DisableAction(clientId, wordList)); - actions.add(new ActionBatch.StartDeleteAction(clientId, wordList)); + actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData)); + actions.add(new ActionBatch.StartDeleteAction(clientId, wordListMetaData)); actions.execute(context, new LogProblemReporter(TAG)); signalNewDictionaryState(context); } @@ -1076,33 +1093,47 @@ public final class UpdateHandler { */ public static void markAsDeleted(final Context context, final String clientId, final String wordlistId, final int version, final int status) { - final List<WordListMetadata> currentMetadata = - MetadataHandler.getCurrentMetadata(context, clientId); - final WordListMetadata wordList = - MetadataHandler.findWordListById(currentMetadata, wordlistId); - if (null == wordList) return; + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; + final ActionBatch actions = new ActionBatch(); - actions.add(new ActionBatch.FinishDeleteAction(clientId, wordList)); + actions.add(new ActionBatch.FinishDeleteAction(clientId, wordListMetaData)); actions.execute(context, new LogProblemReporter(TAG)); signalNewDictionaryState(context); } /** - * Marks the word list with the passed id as broken. - * - * This effectively deletes the entry from the metadata. It doesn't prevent the same - * word list to be downloaded again at a later time if the same or a new version is - * available the next time we download the metadata. + * Checks whether the word list should be downloaded again; in which case an download & + * installation attempt is made. Otherwise the word list is marked broken. * * @param context the context to open the database on. * @param clientId the id of the client. - * @param wordlistId the id of the word list to mark as broken. - * @param version the version of the word list to mark as deleted. + * @param wordlistId the id of the word list which is broken. + * @param version the version of the broken word list. */ - public static void markAsBroken(final Context context, final String clientId, + public static void markAsBrokenOrRetrying(final Context context, final String clientId, final String wordlistId, final int version) { - // TODO: do this on another thread to avoid blocking the UI. - MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId), - wordlistId, version); + boolean isRetryPossible = MetadataDbHelper.maybeMarkEntryAsRetrying( + MetadataDbHelper.getDb(context, clientId), wordlistId, version); + + if (isRetryPossible) { + if (DEBUG) { + Log.d(TAG, "Attempting to download & install the wordlist again."); + } + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + final ActionBatch actions = new ActionBatch(); + actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData, false)); + actions.execute(context, new LogProblemReporter(TAG)); + } else { + if (DEBUG) { + Log.d(TAG, "Retries for wordlist exhausted, deleting the wordlist from table."); + } + MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId), + wordlistId, version); + } } } diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java index 9e510a68b..59f75e4ed 100644 --- a/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java +++ b/java/src/com/android/inputmethod/dictionarypack/WordListMetadata.java @@ -36,6 +36,7 @@ public class WordListMetadata { public final String mRemoteFilename; public final int mVersion; // version of this word list public final int mFlags; // Always 0 in this version, reserved for future use + public int mRetryCount; // The locale is matched against the locale requested by the client. The matching algorithm // is a standard locale matching with fallback; it is implemented in @@ -51,8 +52,9 @@ public class WordListMetadata { public WordListMetadata(final String id, final int type, final String description, final long lastUpdate, final long fileSize, - final String rawChecksum, final String checksum, final String localFilename, - final String remoteFilename, final int version, final int formatVersion, + final String rawChecksum, final String checksum, final int retryCount, + final String localFilename, final String remoteFilename, + final int version, final int formatVersion, final int flags, final String locale) { mId = id; mType = type; @@ -61,6 +63,7 @@ public class WordListMetadata { mFileSize = fileSize; mRawChecksum = rawChecksum; mChecksum = checksum; + mRetryCount = retryCount; mLocalFilename = localFilename; mRemoteFilename = remoteFilename; mVersion = version; @@ -82,6 +85,7 @@ public class WordListMetadata { final Long fileSize = values.getAsLong(MetadataDbHelper.FILESIZE_COLUMN); final String rawChecksum = values.getAsString(MetadataDbHelper.RAW_CHECKSUM_COLUMN); final String checksum = values.getAsString(MetadataDbHelper.CHECKSUM_COLUMN); + final int retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN); final String localFilename = values.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN); final String remoteFilename = values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN); final Integer version = values.getAsInteger(MetadataDbHelper.VERSION_COLUMN); @@ -103,7 +107,8 @@ public class WordListMetadata { throw new IllegalArgumentException(); } return new WordListMetadata(id, type, description, lastUpdate, fileSize, rawChecksum, - checksum, localFilename, remoteFilename, version, formatVersion, flags, locale); + checksum, retryCount, localFilename, remoteFilename, version, formatVersion, + flags, locale); } @Override @@ -116,6 +121,7 @@ public class WordListMetadata { sb.append("\nFileSize : ").append(mFileSize); sb.append("\nRawChecksum : ").append(mRawChecksum); sb.append("\nChecksum : ").append(mChecksum); + sb.append("\nRetryCount: ").append(mRetryCount); sb.append("\nLocalFilename : ").append(mLocalFilename); sb.append("\nRemoteFilename : ").append(mRemoteFilename); sb.append("\nVersion : ").append(mVersion); diff --git a/java/src/com/android/inputmethod/event/DeadKeyCombiner.java b/java/src/com/android/inputmethod/event/DeadKeyCombiner.java index 4f3f4d25f..88c70630d 100644 --- a/java/src/com/android/inputmethod/event/DeadKeyCombiner.java +++ b/java/src/com/android/inputmethod/event/DeadKeyCombiner.java @@ -17,10 +17,12 @@ package com.android.inputmethod.event; import android.text.TextUtils; +import android.util.SparseIntArray; import android.view.KeyCharacterMap; import com.android.inputmethod.latin.Constants; +import java.text.Normalizer; import java.util.ArrayList; import javax.annotation.Nonnull; @@ -29,9 +31,209 @@ import javax.annotation.Nonnull; * A combiner that handles dead keys. */ public class DeadKeyCombiner implements Combiner { + + private static class Data { + // This class data taken from KeyCharacterMap.java. + + /* Characters used to display placeholders for dead keys. */ + private static final int ACCENT_ACUTE = '\u00B4'; + private static final int ACCENT_BREVE = '\u02D8'; + private static final int ACCENT_CARON = '\u02C7'; + private static final int ACCENT_CEDILLA = '\u00B8'; + private static final int ACCENT_CIRCUMFLEX = '\u02C6'; + private static final int ACCENT_COMMA_ABOVE = '\u1FBD'; + private static final int ACCENT_COMMA_ABOVE_RIGHT = '\u02BC'; + private static final int ACCENT_DOT_ABOVE = '\u02D9'; + private static final int ACCENT_DOT_BELOW = Constants.CODE_PERIOD; // approximate + private static final int ACCENT_DOUBLE_ACUTE = '\u02DD'; + private static final int ACCENT_GRAVE = '\u02CB'; + private static final int ACCENT_HOOK_ABOVE = '\u02C0'; + private static final int ACCENT_HORN = Constants.CODE_SINGLE_QUOTE; // approximate + private static final int ACCENT_MACRON = '\u00AF'; + private static final int ACCENT_MACRON_BELOW = '\u02CD'; + private static final int ACCENT_OGONEK = '\u02DB'; + private static final int ACCENT_REVERSED_COMMA_ABOVE = '\u02BD'; + private static final int ACCENT_RING_ABOVE = '\u02DA'; + private static final int ACCENT_STROKE = Constants.CODE_DASH; // approximate + private static final int ACCENT_TILDE = '\u02DC'; + private static final int ACCENT_TURNED_COMMA_ABOVE = '\u02BB'; + private static final int ACCENT_UMLAUT = '\u00A8'; + private static final int ACCENT_VERTICAL_LINE_ABOVE = '\u02C8'; + private static final int ACCENT_VERTICAL_LINE_BELOW = '\u02CC'; + + /* Legacy dead key display characters used in previous versions of the API (before L) + * We still support these characters by mapping them to their non-legacy version. */ + private static final int ACCENT_GRAVE_LEGACY = Constants.CODE_GRAVE_ACCENT; + private static final int ACCENT_CIRCUMFLEX_LEGACY = Constants.CODE_CIRCUMFLEX_ACCENT; + private static final int ACCENT_TILDE_LEGACY = Constants.CODE_TILDE; + + /** + * Maps Unicode combining diacritical to display-form dead key. + */ + private static final SparseIntArray sCombiningToAccent = new SparseIntArray(); + private static final SparseIntArray sAccentToCombining = new SparseIntArray(); + static { + // U+0300: COMBINING GRAVE ACCENT + addCombining('\u0300', ACCENT_GRAVE); + // U+0301: COMBINING ACUTE ACCENT + addCombining('\u0301', ACCENT_ACUTE); + // U+0302: COMBINING CIRCUMFLEX ACCENT + addCombining('\u0302', ACCENT_CIRCUMFLEX); + // U+0303: COMBINING TILDE + addCombining('\u0303', ACCENT_TILDE); + // U+0304: COMBINING MACRON + addCombining('\u0304', ACCENT_MACRON); + // U+0306: COMBINING BREVE + addCombining('\u0306', ACCENT_BREVE); + // U+0307: COMBINING DOT ABOVE + addCombining('\u0307', ACCENT_DOT_ABOVE); + // U+0308: COMBINING DIAERESIS + addCombining('\u0308', ACCENT_UMLAUT); + // U+0309: COMBINING HOOK ABOVE + addCombining('\u0309', ACCENT_HOOK_ABOVE); + // U+030A: COMBINING RING ABOVE + addCombining('\u030A', ACCENT_RING_ABOVE); + // U+030B: COMBINING DOUBLE ACUTE ACCENT + addCombining('\u030B', ACCENT_DOUBLE_ACUTE); + // U+030C: COMBINING CARON + addCombining('\u030C', ACCENT_CARON); + // U+030D: COMBINING VERTICAL LINE ABOVE + addCombining('\u030D', ACCENT_VERTICAL_LINE_ABOVE); + // U+030E: COMBINING DOUBLE VERTICAL LINE ABOVE + //addCombining('\u030E', ACCENT_DOUBLE_VERTICAL_LINE_ABOVE); + // U+030F: COMBINING DOUBLE GRAVE ACCENT + //addCombining('\u030F', ACCENT_DOUBLE_GRAVE); + // U+0310: COMBINING CANDRABINDU + //addCombining('\u0310', ACCENT_CANDRABINDU); + // U+0311: COMBINING INVERTED BREVE + //addCombining('\u0311', ACCENT_INVERTED_BREVE); + // U+0312: COMBINING TURNED COMMA ABOVE + addCombining('\u0312', ACCENT_TURNED_COMMA_ABOVE); + // U+0313: COMBINING COMMA ABOVE + addCombining('\u0313', ACCENT_COMMA_ABOVE); + // U+0314: COMBINING REVERSED COMMA ABOVE + addCombining('\u0314', ACCENT_REVERSED_COMMA_ABOVE); + // U+0315: COMBINING COMMA ABOVE RIGHT + addCombining('\u0315', ACCENT_COMMA_ABOVE_RIGHT); + // U+031B: COMBINING HORN + addCombining('\u031B', ACCENT_HORN); + // U+0323: COMBINING DOT BELOW + addCombining('\u0323', ACCENT_DOT_BELOW); + // U+0326: COMBINING COMMA BELOW + //addCombining('\u0326', ACCENT_COMMA_BELOW); + // U+0327: COMBINING CEDILLA + addCombining('\u0327', ACCENT_CEDILLA); + // U+0328: COMBINING OGONEK + addCombining('\u0328', ACCENT_OGONEK); + // U+0329: COMBINING VERTICAL LINE BELOW + addCombining('\u0329', ACCENT_VERTICAL_LINE_BELOW); + // U+0331: COMBINING MACRON BELOW + addCombining('\u0331', ACCENT_MACRON_BELOW); + // U+0335: COMBINING SHORT STROKE OVERLAY + addCombining('\u0335', ACCENT_STROKE); + // U+0342: COMBINING GREEK PERISPOMENI + //addCombining('\u0342', ACCENT_PERISPOMENI); + // U+0344: COMBINING GREEK DIALYTIKA TONOS + //addCombining('\u0344', ACCENT_DIALYTIKA_TONOS); + // U+0345: COMBINING GREEK YPOGEGRAMMENI + //addCombining('\u0345', ACCENT_YPOGEGRAMMENI); + + // One-way mappings to equivalent preferred accents. + // U+0340: COMBINING GRAVE TONE MARK + sCombiningToAccent.append('\u0340', ACCENT_GRAVE); + // U+0341: COMBINING ACUTE TONE MARK + sCombiningToAccent.append('\u0341', ACCENT_ACUTE); + // U+0343: COMBINING GREEK KORONIS + sCombiningToAccent.append('\u0343', ACCENT_COMMA_ABOVE); + + // One-way legacy mappings to preserve compatibility with older applications. + // U+0300: COMBINING GRAVE ACCENT + sAccentToCombining.append(ACCENT_GRAVE_LEGACY, '\u0300'); + // U+0302: COMBINING CIRCUMFLEX ACCENT + sAccentToCombining.append(ACCENT_CIRCUMFLEX_LEGACY, '\u0302'); + // U+0303: COMBINING TILDE + sAccentToCombining.append(ACCENT_TILDE_LEGACY, '\u0303'); + } + + private static void addCombining(int combining, int accent) { + sCombiningToAccent.append(combining, accent); + sAccentToCombining.append(accent, combining); + } + + // Caution! This may only contain chars, not supplementary code points. It's unlikely + // it will ever need to, but if it does we'll have to change this + private static final SparseIntArray sNonstandardDeadCombinations = new SparseIntArray(); + static { + // Non-standard decompositions. + // Stroke modifier for Finnish multilingual keyboard and others. + // U+0110: LATIN CAPITAL LETTER D WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'D', '\u0110'); + // U+01E4: LATIN CAPITAL LETTER G WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'G', '\u01e4'); + // U+0126: LATIN CAPITAL LETTER H WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'H', '\u0126'); + // U+0197: LATIN CAPITAL LETTER I WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'I', '\u0197'); + // U+0141: LATIN CAPITAL LETTER L WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'L', '\u0141'); + // U+00D8: LATIN CAPITAL LETTER O WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'O', '\u00d8'); + // U+0166: LATIN CAPITAL LETTER T WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'T', '\u0166'); + // U+0111: LATIN SMALL LETTER D WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'd', '\u0111'); + // U+01E5: LATIN SMALL LETTER G WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'g', '\u01e5'); + // U+0127: LATIN SMALL LETTER H WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'h', '\u0127'); + // U+0268: LATIN SMALL LETTER I WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'i', '\u0268'); + // U+0142: LATIN SMALL LETTER L WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'l', '\u0142'); + // U+00F8: LATIN SMALL LETTER O WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'o', '\u00f8'); + // U+0167: LATIN SMALL LETTER T WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 't', '\u0167'); + } + + private static void addNonStandardDeadCombination(final int deadCodePoint, + final int spacingCodePoint, final int result) { + final int combination = (deadCodePoint << 16) | spacingCodePoint; + sNonstandardDeadCombinations.put(combination, result); + } + + public static final int NOT_A_CHAR = 0; + public static final int BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION = 16; + // Get a non-standard combination + public static char getNonstandardCombination(final int deadCodePoint, + final int spacingCodePoint) { + final int combination = spacingCodePoint | + (deadCodePoint << BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION); + return (char)sNonstandardDeadCombinations.get(combination, NOT_A_CHAR); + } + } + // TODO: make this a list of events instead final StringBuilder mDeadSequence = new StringBuilder(); + @Nonnull + private Event createEventChainFromSequence(final @Nonnull CharSequence text, + final Event originalEvent) { + if (text.length() <= 0) { + return originalEvent; + } else { + Event lastEvent = null; + int codePoint = 0; + for (int i = text.length(); i > 0; i -= Character.charCount(codePoint)) { + codePoint = Character.codePointBefore(text, i); + final Event thisEvent = Event.createHardwareKeypressEvent(codePoint, + originalEvent.mKeyCode, lastEvent, false /* isKeyRepeat */); + lastEvent = thisEvent; + } + return lastEvent; + } + } + @Override @Nonnull public Event processEvent(final ArrayList<Event> previousEvents, final Event event) { @@ -47,29 +249,48 @@ public class DeadKeyCombiner implements Combiner { // simply returns the event as is. The majority of events will go through this path. return event; } else { - // TODO: Allow combining for several dead chars rather than only the first one. - // The framework doesn't know how to do this now. - final int deadCodePoint = mDeadSequence.codePointAt(0); - mDeadSequence.setLength(0); - final int resultingCodePoint = - KeyCharacterMap.getDeadChar(deadCodePoint, event.mCodePoint); - if (0 == resultingCodePoint) { - // We can't combine both characters. We need to commit the dead key as a separate - // character, and the next char too unless it's a space (because as a special case, - // dead key + space should result in only the dead key being committed - that's - // how dead keys work). - // If the event is a space, we should commit the dead char alone, but if it's - // not, we need to commit both. - // TODO: this is not necessarily triggered by hardware key events, so it's not - // a good idea to masquerade as one. This should be typed as a software - // composite event or something. - return Event.createHardwareKeypressEvent(deadCodePoint, event.mKeyCode, - Constants.CODE_SPACE == event.mCodePoint ? null : event /* next */, - false /* isKeyRepeat */); + if (Character.isWhitespace(event.mCodePoint) + || event.mCodePoint == mDeadSequence.codePointBefore(mDeadSequence.length())) { + // When whitespace or twice the same dead key, we should output the dead sequence + // as is. + final Event resultEvent = createEventChainFromSequence(mDeadSequence.toString(), + event); + mDeadSequence.setLength(0); + return resultEvent; + } else if (event.isFunctionalKeyEvent()) { + if (Constants.CODE_DELETE == event.mKeyCode) { + // Remove the last code point + final int trimIndex = mDeadSequence.length() - Character.charCount( + mDeadSequence.codePointBefore(mDeadSequence.length())); + mDeadSequence.setLength(trimIndex); + return Event.createConsumedEvent(event); + } else { + return event; + } + } else if (event.isDead()) { + mDeadSequence.appendCodePoint(event.mCodePoint); + return Event.createConsumedEvent(event); } else { - // We could combine the characters. - return Event.createHardwareKeypressEvent(resultingCodePoint, event.mKeyCode, - null /* next */, false /* isKeyRepeat */); + // Combine normally. + final StringBuilder sb = new StringBuilder(); + sb.appendCodePoint(event.mCodePoint); + int codePointIndex = 0; + while (codePointIndex < mDeadSequence.length()) { + final int deadCodePoint = mDeadSequence.codePointAt(codePointIndex); + final char replacementSpacingChar = + Data.getNonstandardCombination(deadCodePoint, event.mCodePoint); + if (Data.NOT_A_CHAR != replacementSpacingChar) { + sb.setCharAt(0, replacementSpacingChar); + } else { + final int combining = Data.sAccentToCombining.get(deadCodePoint); + sb.appendCodePoint(0 == combining ? deadCodePoint : combining); + } + codePointIndex += Character.isSupplementaryCodePoint(deadCodePoint) ? 2 : 1; + } + final String normalizedString = Normalizer.normalize(sb, Normalizer.Form.NFC); + final Event resultEvent = createEventChainFromSequence(normalizedString, event); + mDeadSequence.setLength(0); + return resultEvent; } } } diff --git a/java/src/com/android/inputmethod/event/Event.java b/java/src/com/android/inputmethod/event/Event.java index ef5b04747..ff6f88066 100644 --- a/java/src/com/android/inputmethod/event/Event.java +++ b/java/src/com/android/inputmethod/event/Event.java @@ -16,6 +16,7 @@ package com.android.inputmethod.event; +import com.android.inputmethod.annotations.ExternallyReferenced; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.utils.StringUtils; @@ -147,6 +148,7 @@ public class Event { } // This creates an input event for a dead character. @see {@link #FLAG_DEAD} + @ExternallyReferenced public static Event createDeadEvent(final int codePoint, final int keyCode, final Event next) { // TODO: add an argument or something if we ever create a software layout with dead keys. return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index 81ea90a4d..45ce6a85f 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -94,13 +94,23 @@ public class Key implements Comparable<Key> { /** Icon to display instead of a label. Icon takes precedence over a label */ private final int mIconId; - /** Width of the key, not including the gap */ + /** Width of the key, excluding the gap */ private final int mWidth; - /** Height of the key, not including the gap */ + /** Height of the key, excluding the gap */ private final int mHeight; - /** X coordinate of the key in the keyboard layout */ + /** + * The combined width in pixels of the horizontal gaps belonging to this key, both to the left + * and to the right. I.e., mWidth + mHorizontalGap = total width belonging to the key. + */ + private final int mHorizontalGap; + /** + * The combined height in pixels of the vertical gaps belonging to this key, both above and + * below. I.e., mHeight + mVerticalGap = total height belonging to the key. + */ + private final int mVerticalGap; + /** X coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ private final int mX; - /** Y coordinate of the key in the keyboard layout */ + /** Y coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ private final int mY; /** Hit bounding box of the key */ private final Rect mHitBox = new Rect(); @@ -198,8 +208,10 @@ public class Key implements Comparable<Key> { final String hintLabel, final int labelFlags, final int backgroundType, final int x, final int y, final int width, final int height, final int horizontalGap, final int verticalGap) { - mHeight = height - verticalGap; mWidth = width - horizontalGap; + mHeight = height - verticalGap; + mHorizontalGap = horizontalGap; + mVerticalGap = verticalGap; mHintLabel = hintLabel; mLabelFlags = labelFlags; mBackgroundType = backgroundType; @@ -214,7 +226,7 @@ public class Key implements Comparable<Key> { mEnabled = (code != CODE_UNSPECIFIED); mIconId = iconId; // Horizontal gap is divided equally to both sides of the key. - mX = x + horizontalGap / 2; + mX = x + mHorizontalGap / 2; mY = y; mHitBox.set(x, y, x + width + 1, y + height); mKeyVisualAttributes = null; @@ -235,18 +247,21 @@ public class Key implements Comparable<Key> { */ public Key(final String keySpec, final TypedArray keyAttr, final KeyStyle style, final KeyboardParams params, final KeyboardRow row) { - final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap; + mHorizontalGap = isSpacer() ? 0 : params.mHorizontalGap; + mVerticalGap = params.mVerticalGap; + + final float horizontalGapFloat = mHorizontalGap; final int rowHeight = row.getRowHeight(); - mHeight = rowHeight - params.mVerticalGap; + mHeight = rowHeight - mVerticalGap; final float keyXPos = row.getKeyX(keyAttr); final float keyWidth = row.getKeyWidth(keyAttr, keyXPos); final int keyYPos = row.getKeyY(); // Horizontal gap is divided equally to both sides of the key. - mX = Math.round(keyXPos + horizontalGap / 2); + mX = Math.round(keyXPos + horizontalGapFloat / 2); mY = keyYPos; - mWidth = Math.round(keyWidth - horizontalGap); + mWidth = Math.round(keyWidth - horizontalGapFloat); mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1, keyYPos + rowHeight); // Update row to have current x coordinate. @@ -263,8 +278,8 @@ public class Key implements Comparable<Key> { mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags) | row.getDefaultKeyLabelFlags(); - final boolean needsToUpperCase = needsToUpperCase(mLabelFlags, params.mId.mElementId); - final Locale locale = params.mId.mLocale; + final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId); + final Locale localeForUpcasing = params.mId.getLocales()[0]; int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags); String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys); @@ -306,7 +321,7 @@ public class Key implements Comparable<Key> { actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS; mMoreKeys = new MoreKeySpec[moreKeys.length]; for (int i = 0; i < moreKeys.length; i++) { - mMoreKeys[i] = new MoreKeySpec(moreKeys[i], needsToUpperCase, locale); + mMoreKeys[i] = new MoreKeySpec(moreKeys[i], needsToUpcase, localeForUpcasing); } } else { mMoreKeys = null; @@ -327,16 +342,16 @@ public class Key implements Comparable<Key> { mLabel = new StringBuilder().appendCodePoint(code).toString(); } else { mLabel = StringUtils.toUpperCaseOfStringForLocale( - KeySpecParser.getLabel(keySpec), needsToUpperCase, locale); + KeySpecParser.getLabel(keySpec), needsToUpcase, localeForUpcasing); } if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) { mHintLabel = null; } else { mHintLabel = StringUtils.toUpperCaseOfStringForLocale(style.getString(keyAttr, - R.styleable.Keyboard_Key_keyHintLabel), needsToUpperCase, locale); + R.styleable.Keyboard_Key_keyHintLabel), needsToUpcase, localeForUpcasing); } String outputText = StringUtils.toUpperCaseOfStringForLocale( - KeySpecParser.getOutputText(keySpec), needsToUpperCase, locale); + KeySpecParser.getOutputText(keySpec), needsToUpcase, localeForUpcasing); // Choose the first letter of the label as primary code if not specified. if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText) && !TextUtils.isEmpty(mLabel)) { @@ -362,12 +377,12 @@ public class Key implements Comparable<Key> { mCode = CODE_OUTPUT_TEXT; } } else { - mCode = StringUtils.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale); + mCode = StringUtils.toUpperCaseOfCodeForLocale(code, needsToUpcase, localeForUpcasing); } final int altCodeInAttr = KeySpecParser.parseCode( style.getString(keyAttr, R.styleable.Keyboard_Key_altCode), CODE_UNSPECIFIED); final int altCode = StringUtils.toUpperCaseOfCodeForLocale( - altCodeInAttr, needsToUpperCase, locale); + altCodeInAttr, needsToUpcase, localeForUpcasing); mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode, disabledIconId, visualInsetsLeft, visualInsetsRight); mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); @@ -380,6 +395,10 @@ public class Key implements Comparable<Key> { * @param key the original key. */ protected Key(final Key key) { + this(key, key.mMoreKeys); + } + + private Key(final Key key, final MoreKeySpec[] moreKeys) { // Final attributes. mCode = key.mCode; mLabel = key.mLabel; @@ -388,10 +407,12 @@ public class Key implements Comparable<Key> { mIconId = key.mIconId; mWidth = key.mWidth; mHeight = key.mHeight; + mHorizontalGap = key.mHorizontalGap; + mVerticalGap = key.mVerticalGap; mX = key.mX; mY = key.mY; mHitBox.set(key.mHitBox); - mMoreKeys = key.mMoreKeys; + mMoreKeys = moreKeys; mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags; mBackgroundType = key.mBackgroundType; mActionFlags = key.mActionFlags; @@ -403,7 +424,15 @@ public class Key implements Comparable<Key> { mEnabled = key.mEnabled; } - private static boolean needsToUpperCase(final int labelFlags, final int keyboardElementId) { + public static Key removeRedundantMoreKeys(final Key key, + final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout) { + final MoreKeySpec[] moreKeys = key.getMoreKeys(); + final MoreKeySpec[] filteredMoreKeys = MoreKeySpec.removeRedundantMoreKeys( + moreKeys, lettersOnBaseLayout); + return (filteredMoreKeys == moreKeys) ? key : new Key(key, filteredMoreKeys); + } + + private static boolean needsToUpcase(final int labelFlags, final int keyboardElementId) { if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false; switch (keyboardElementId) { case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: @@ -771,18 +800,52 @@ public class Key implements Comparable<Key> { return iconSet.getIconDrawable(getIconId()); } + /** + * Gets the width of the key in pixels, excluding the gap. + * @return The width of the key in pixels, excluding the gap. + */ public int getWidth() { return mWidth; } + /** + * Gets the height of the key in pixels, excluding the gap. + * @return The height of the key in pixels, excluding the gap. + */ public int getHeight() { return mHeight; } + /** + * The combined width in pixels of the horizontal gaps belonging to this key, both above and + * below. I.e., getWidth() + getHorizontalGap() = total width belonging to the key. + * @return Horizontal gap belonging to this key. + */ + public int getHorizontalGap() { + return mHorizontalGap; + } + + /** + * The combined height in pixels of the vertical gaps belonging to this key, both above and + * below. I.e., getHeight() + getVerticalGap() = total height belonging to the key. + * @return Vertical gap belonging to this key. + */ + public int getVerticalGap() { + return mVerticalGap; + } + + /** + * Gets the x-coordinate of the top-left corner of the key in pixels, excluding the gap. + * @return The x-coordinate of the top-left corner of the key in pixels, excluding the gap. + */ public int getX() { return mX; } + /** + * Gets the y-coordinate of the top-left corner of the key in pixels, excluding the gap. + * @return The y-coordinate of the top-left corner of the key in pixels, excluding the gap. + */ public int getY() { return mY; } diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java index 85dfea4e7..d35c8fae1 100644 --- a/java/src/com/android/inputmethod/keyboard/Keyboard.java +++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java @@ -108,10 +108,9 @@ public class Keyboard { mAltCodeKeysWhileTyping = Collections.unmodifiableList(params.mAltCodeKeysWhileTyping); mIconsSet = params.mIconsSet; - mProximityInfo = new ProximityInfo(params.mId.mLocale.toString(), - params.GRID_WIDTH, params.GRID_HEIGHT, mOccupiedWidth, mOccupiedHeight, - mMostCommonKeyWidth, mMostCommonKeyHeight, mSortedKeys, - params.mTouchPositionCorrection); + mProximityInfo = new ProximityInfo(params.GRID_WIDTH, params.GRID_HEIGHT, + mOccupiedWidth, mOccupiedHeight, mMostCommonKeyWidth, mMostCommonKeyHeight, + mSortedKeys, params.mTouchPositionCorrection); mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled; } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java index 3c1167538..ab0d63306 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java @@ -21,9 +21,9 @@ import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOAR import android.text.InputType; import android.text.TextUtils; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.compat.EditorInfoCompatUtils; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.utils.InputTypeUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; @@ -62,8 +62,7 @@ public final class KeyboardId { public static final int ELEMENT_EMOJI_CATEGORY5 = 15; public static final int ELEMENT_EMOJI_CATEGORY6 = 16; - public final InputMethodSubtype mSubtype; - public final Locale mLocale; + public final RichInputMethodSubtype mSubtype; public final int mWidth; public final int mHeight; public final int mMode; @@ -73,12 +72,12 @@ public final class KeyboardId { public final boolean mLanguageSwitchKeyEnabled; public final String mCustomActionLabel; public final boolean mHasShortcutKey; + public final boolean mIsSplitLayout; private final int mHashCode; public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params) { mSubtype = params.mSubtype; - mLocale = SubtypeLocaleUtils.getSubtypeLocale(mSubtype); mWidth = params.mKeyboardWidth; mHeight = params.mKeyboardHeight; mMode = params.mMode; @@ -89,6 +88,7 @@ public final class KeyboardId { mCustomActionLabel = (mEditorInfo.actionLabel != null) ? mEditorInfo.actionLabel.toString() : null; mHasShortcutKey = params.mVoiceInputKeyEnabled; + mIsSplitLayout = params.mIsSplitLayoutEnabled; mHashCode = computeHashCode(this); } @@ -108,7 +108,8 @@ public final class KeyboardId { id.mCustomActionLabel, id.navigateNext(), id.navigatePrevious(), - id.mSubtype + id.mSubtype, + id.mIsSplitLayout }); } @@ -128,7 +129,8 @@ public final class KeyboardId { && TextUtils.equals(other.mCustomActionLabel, mCustomActionLabel) && other.navigateNext() == navigateNext() && other.navigatePrevious() == navigatePrevious() - && other.mSubtype.equals(mSubtype); + && other.mSubtype.equals(mSubtype) + && other.mIsSplitLayout == mIsSplitLayout; } private static boolean isAlphabetKeyboard(final int elementId) { @@ -163,6 +165,10 @@ public final class KeyboardId { return InputTypeUtils.getImeOptionsActionIdFromEditorInfo(mEditorInfo); } + public Locale[] getLocales() { + return mSubtype.getLocales(); + } + @Override public boolean equals(final Object other) { return other instanceof KeyboardId && equals((KeyboardId) other); @@ -175,9 +181,10 @@ public final class KeyboardId { @Override public String toString() { - return String.format(Locale.ROOT, "[%s %s:%s %dx%d %s %s%s%s%s%s%s%s%s]", + return String.format(Locale.ROOT, "[%s %s:%s %dx%d %s %s%s%s%s%s%s%s%s%s]", elementIdToName(mElementId), - mLocale, mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), + Arrays.deepToString(mSubtype.getLocales()), + mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), mWidth, mHeight, modeName(mMode), actionName(imeAction()), @@ -187,7 +194,8 @@ public final class KeyboardId { (passwordInput() ? " passwordInput" : ""), (mHasShortcutKey ? " hasShortcutKey" : ""), (mLanguageSwitchKeyEnabled ? " languageSwitchKeyEnabled" : ""), - (isMultiLine() ? " isMultiLine" : "") + (isMultiLine() ? " isMultiLine" : ""), + (mIsSplitLayout ? " isSplitLayout" : "") ); } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java index feb79efe9..1c66c37d3 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java @@ -37,8 +37,10 @@ import com.android.inputmethod.keyboard.internal.KeyboardParams; import com.android.inputmethod.keyboard.internal.KeysCache; import com.android.inputmethod.latin.InputAttributes; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.SubtypeSwitcher; import com.android.inputmethod.latin.define.DebugFlags; +import com.android.inputmethod.latin.utils.DebugLogUtils; import com.android.inputmethod.latin.utils.InputTypeUtils; import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; @@ -96,6 +98,8 @@ public final class KeyboardLayoutSet { private static final class ElementParams { int mKeyboardXmlId; boolean mProximityCharsCorrectionEnabled; + boolean mSupportsSplitLayout; + boolean mAllowRedundantMoreKeys; public ElementParams() {} } @@ -109,11 +113,17 @@ public final class KeyboardLayoutSet { boolean mVoiceInputKeyEnabled; boolean mNoSettingsKey; boolean mLanguageSwitchKeyEnabled; - InputMethodSubtype mSubtype; + RichInputMethodSubtype mSubtype; boolean mIsSpellChecker; int mKeyboardWidth; int mKeyboardHeight; int mScriptId = ScriptUtils.SCRIPT_LATIN; + // Indicates if the user has enabled the split-layout preference + // and the required ProductionFlags are enabled. + boolean mIsSplitLayoutEnabledByUser; + // Indicates if split layout is actually enabled, taking into account + // whether the user has enabled it, and the keyboard layout supports it. + boolean mIsSplitLayoutEnabled; // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap = new SparseArray<>(); @@ -168,6 +178,9 @@ public final class KeyboardLayoutSet { // attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is // specified as an elementKeyboard attribute in the file. // The KeyboardId is an internal key for a Keyboard object. + + mParams.mIsSplitLayoutEnabled = mParams.mIsSplitLayoutEnabledByUser + && elementParams.mSupportsSplitLayout; final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams); try { return getKeyboard(elementParams, id); @@ -192,6 +205,7 @@ public final class KeyboardLayoutSet { if (id.isAlphabetKeyboard()) { builder.setAutoGenerate(sKeysCache); } + builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys); final int keyboardXmlId = elementParams.mKeyboardXmlId; builder.load(keyboardXmlId, id); if (mParams.mDisableTouchPositionCorrectionDataForTest) { @@ -253,7 +267,7 @@ public final class KeyboardLayoutSet { return this; } - public Builder setSubtype(final InputMethodSubtype subtype) { + public Builder setSubtype(final RichInputMethodSubtype subtype) { final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype); // TODO: Consolidate with {@link InputAttributes}. @SuppressWarnings("deprecation") @@ -262,7 +276,7 @@ public final class KeyboardLayoutSet { final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii( mParams.mEditorInfo.imeOptions) || deprecatedForceAscii; - final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) + final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) ? SubtypeSwitcher.getInstance().getNoLanguageSubtype() : subtype; mParams.mSubtype = keyboardSubtype; @@ -286,31 +300,93 @@ public final class KeyboardLayoutSet { return this; } - public void disableTouchPositionCorrectionData() { + public Builder disableTouchPositionCorrectionData() { mParams.mDisableTouchPositionCorrectionDataForTest = true; + return this; } - public void setScriptId(final int scriptId) { + public Builder setScriptId(final int scriptId) { mParams.mScriptId = scriptId; + return this; + } + + public Builder setSplitLayoutEnabledByUser(final boolean enabled) { + mParams.mIsSplitLayoutEnabledByUser = enabled; + return this; + } + + private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes = + new HashMap<>(); + public static int getScriptId(final Resources resources, final InputMethodSubtype subtype) { + final Integer value = sScriptIdsForSubtypes.get(subtype); + if (null == value) { + final int scriptId = readScriptId(resources, subtype); + sScriptIdsForSubtypes.put(subtype, scriptId); + return scriptId; + } + return value; + } + + // Super redux version of reading the script ID for some subtype from Xml. + private static int readScriptId(final Resources resources, + final InputMethodSubtype subtype) { + final String layoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX + + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype); + final int xmlId = getXmlId(resources, layoutSetName); + final XmlResourceParser parser = resources.getXml(xmlId); + try { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + // Bovinate through the XML stupidly searching for TAG_FEATURE, and read + // the script Id from it. + parser.next(); + final String tag = parser.getName(); + if (TAG_FEATURE.equals(tag)) { + return readScriptIdFromTagFeature(resources, parser); + } + } + } catch (final IOException | XmlPullParserException e) { + throw new RuntimeException(e.getMessage() + " in " + layoutSetName, e); + } finally { + parser.close(); + } + // If the tag is not found, then the default script is Latin. + return ScriptUtils.SCRIPT_LATIN; + } + + private static int readScriptIdFromTagFeature(final Resources resources, + final XmlPullParser parser) throws IOException, XmlPullParserException { + final TypedArray featureAttr = resources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.KeyboardLayoutSet_Feature); + try { + final int scriptId = + featureAttr.getInt(R.styleable.KeyboardLayoutSet_Feature_supportedScript, + ScriptUtils.SCRIPT_UNKNOWN); + XmlParseUtils.checkEndTag(TAG_FEATURE, parser); + return scriptId; + } finally { + featureAttr.recycle(); + } } public KeyboardLayoutSet build() { if (mParams.mSubtype == null) throw new RuntimeException("KeyboardLayoutSet subtype is not specified"); - final String packageName = mResources.getResourcePackageName( - R.xml.keyboard_layout_set_qwerty); - final String keyboardLayoutSetName = mParams.mKeyboardLayoutSetName; - final int xmlId = mResources.getIdentifier(keyboardLayoutSetName, "xml", packageName); + final int xmlId = getXmlId(mResources, mParams.mKeyboardLayoutSetName); try { parseKeyboardLayoutSet(mResources, xmlId); - } catch (final IOException e) { - throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); - } catch (final XmlPullParserException e) { - throw new RuntimeException(e.getMessage() + " in " + keyboardLayoutSetName, e); + } catch (final IOException | XmlPullParserException e) { + throw new RuntimeException(e.getMessage() + " in " + mParams.mKeyboardLayoutSetName, + e); } return new KeyboardLayoutSet(mContext, mParams); } + private static int getXmlId(final Resources resources, final String keyboardLayoutSetName) { + final String packageName = resources.getResourcePackageName( + R.xml.keyboard_layout_set_qwerty); + return resources.getIdentifier(keyboardLayoutSetName, "xml", packageName); + } + private void parseKeyboardLayoutSet(final Resources res, final int resId) throws XmlPullParserException, IOException { final XmlResourceParser parser = res.getXml(resId); @@ -376,6 +452,10 @@ public final class KeyboardLayoutSet { elementParams.mProximityCharsCorrectionEnabled = a.getBoolean( R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection, false); + elementParams.mSupportsSplitLayout = a.getBoolean( + R.styleable.KeyboardLayoutSet_Element_supportsSplitLayout, false); + elementParams.mAllowRedundantMoreKeys = a.getBoolean( + R.styleable.KeyboardLayoutSet_Element_allowRedundantMoreKeys, true); mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams); } finally { a.recycle(); @@ -384,17 +464,8 @@ public final class KeyboardLayoutSet { private void parseKeyboardLayoutSetFeature(final XmlPullParser parser) throws XmlPullParserException, IOException { - final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), - R.styleable.KeyboardLayoutSet_Feature); - try { - final int scriptId = a.getInt( - R.styleable.KeyboardLayoutSet_Feature_supportedScript, - ScriptUtils.SCRIPT_LATIN); - XmlParseUtils.checkEndTag(TAG_FEATURE, parser); - setScriptId(scriptId); - } finally { - a.recycle(); - } + final int scriptId = readScriptIdFromTagFeature(mResources, parser); + setScriptId(scriptId); } private static int getKeyboardMode(final EditorInfo editorInfo) { diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index 60665f8de..93123d1ec 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -17,9 +17,7 @@ package com.android.inputmethod.keyboard; import android.content.Context; -import android.content.SharedPreferences; import android.content.res.Resources; -import android.preference.PreferenceManager; import android.util.Log; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; @@ -27,6 +25,7 @@ import android.view.View; import android.view.inputmethod.EditorInfo; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; +import com.android.inputmethod.event.Event; import com.android.inputmethod.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException; import com.android.inputmethod.keyboard.emoji.EmojiPalettesView; import com.android.inputmethod.keyboard.internal.KeyboardState; @@ -37,6 +36,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.define.ProductionFlags; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.utils.ResourceUtils; @@ -46,7 +46,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { private static final String TAG = KeyboardSwitcher.class.getSimpleName(); private SubtypeSwitcher mSubtypeSwitcher; - private SharedPreferences mPrefs; private InputView mCurrentInputView; private View mMainKeyboardFrame; @@ -75,13 +74,11 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { } public static void init(final LatinIME latinIme) { - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(latinIme); - sInstance.initInternal(latinIme, prefs); + sInstance.initInternal(latinIme); } - private void initInternal(final LatinIME latinIme, final SharedPreferences prefs) { + private void initInternal(final LatinIME latinIme) { mLatinIME = latinIme; - mPrefs = prefs; mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mState = new KeyboardState(this); mIsHardwareAcceleratedDrawingEnabled = @@ -90,7 +87,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { public void updateKeyboardTheme() { final boolean themeUpdated = updateKeyboardThemeAndContextThemeWrapper( - mLatinIME, KeyboardTheme.getKeyboardTheme(mPrefs)); + mLatinIME, KeyboardTheme.getKeyboardTheme(mLatinIME /* context */)); if (themeUpdated && mKeyboardView != null) { mLatinIME.setInputView(onCreateInputView(mIsHardwareAcceleratedDrawingEnabled)); } @@ -118,10 +115,14 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype()); builder.setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey); builder.setLanguageSwitchKeyEnabled(mLatinIME.shouldShowLanguageSwitchKey()); + builder.setSplitLayoutEnabledByUser(ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED + && settingsValues.mIsSplitKeyboardEnabled); mKeyboardLayoutSet = builder.build(); try { mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState); - mKeyboardTextsSet.setLocale(mSubtypeSwitcher.getCurrentSubtypeLocale(), mThemeContext); + // TODO: revisit this for multi-lingual input + mKeyboardTextsSet.setLocale(mSubtypeSwitcher.getCurrentSubtypeLocales()[0], + mThemeContext); } catch (KeyboardLayoutSetException e) { Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause()); return; @@ -162,7 +163,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { currentSettingsValues.mKeyPreviewDismissDuration); keyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady()); final boolean subtypeChanged = (oldKeyboard == null) - || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale); + || !keyboard.mId.mSubtype.equals(oldKeyboard.mId.mSubtype); final int languageOnSpacebarFormatType = mSubtypeSwitcher.getLanguageOnSpacebarFormatType( keyboard.mId.mSubtype); final boolean hasMultipleEnabledIMEsOrSubtypes = RichInputMethodManager.getInstance() @@ -255,8 +256,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { } public void onToggleEmojiKeyboard() { - if (mKeyboardLayoutSet == null || !isShowingEmojiPalettes()) { - mLatinIME.startShowingInputView(); + final boolean needsToLoadKeyboard = (mKeyboardLayoutSet == null); + if (needsToLoadKeyboard || !isShowingEmojiPalettes()) { + mLatinIME.startShowingInputView(needsToLoadKeyboard); setEmojiKeyboard(); } else { mLatinIME.stopShowingInputView(); @@ -304,9 +306,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { /** * Updates state machine to figure out when to automatically switch back to the previous mode. */ - public void onCodeInput(final int code, final int currentAutoCapsState, + public void onEvent(final Event event, final int currentAutoCapsState, final int currentRecapitalizeState) { - mState.onCodeInput(code, currentAutoCapsState, currentRecapitalizeState); + mState.onEvent(event, currentAutoCapsState, currentRecapitalizeState); } public boolean isShowingEmojiPalettes() { @@ -347,7 +349,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { } updateKeyboardThemeAndContextThemeWrapper( - mLatinIME, KeyboardTheme.getKeyboardTheme(mPrefs)); + mLatinIME, KeyboardTheme.getKeyboardTheme(mLatinIME /* context */)); mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate( R.layout.input_view, null); mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame); diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java b/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java index 7161d3f26..8a9688ac4 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java @@ -16,14 +16,16 @@ package com.android.inputmethod.keyboard; +import android.content.Context; import android.content.SharedPreferences; import android.os.Build.VERSION_CODES; +import android.preference.PreferenceManager; import android.util.Log; -import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.BuildCompatUtils; import com.android.inputmethod.latin.R; +import java.util.ArrayList; import java.util.Arrays; public final class KeyboardTheme implements Comparable<KeyboardTheme> { @@ -40,7 +42,10 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> { public static final int THEME_ID_LXX_DARK = 4; public static final int DEFAULT_THEME_ID = THEME_ID_KLP; - private static final KeyboardTheme[] KEYBOARD_THEMES = { + private static KeyboardTheme[] AVAILABLE_KEYBOARD_THEMES; + + /* package private for testing */ + static final KeyboardTheme[] KEYBOARD_THEMES = { new KeyboardTheme(THEME_ID_ICS, "ICS", R.style.KeyboardTheme_ICS, // This has never been selected because we support ICS or later. VERSION_CODES.BASE), @@ -51,6 +56,7 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> { // Default theme for LXX. BuildCompatUtils.VERSION_CODES_LXX), new KeyboardTheme(THEME_ID_LXX_DARK, "LXXDark", R.style.KeyboardTheme_LXX_Dark, + // This has never been selected as default theme. VERSION_CODES.BASE), }; @@ -62,7 +68,7 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> { public final int mThemeId; public final int mStyleId; public final String mThemeName; - private final int mMinApiVersion; + public final int mMinApiVersion; // Note: The themeId should be aligned with "themeId" attribute of Keyboard style // in values/themes-<style>.xml. @@ -92,10 +98,11 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> { return mThemeId; } - @UsedForTesting - static KeyboardTheme searchKeyboardThemeById(final int themeId) { + /* package private for testing */ + static KeyboardTheme searchKeyboardThemeById(final int themeId, + final KeyboardTheme[] availableThemeIds) { // TODO: This search algorithm isn't optimal if there are many themes. - for (final KeyboardTheme theme : KEYBOARD_THEMES) { + for (final KeyboardTheme theme : availableThemeIds) { if (theme.mThemeId == themeId) { return theme; } @@ -103,15 +110,16 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> { return null; } - @UsedForTesting + /* package private for testing */ static KeyboardTheme getDefaultKeyboardTheme(final SharedPreferences prefs, - final int sdkVersion) { + final int sdkVersion, final KeyboardTheme[] availableThemeArray) { final String klpThemeIdString = prefs.getString(KLP_KEYBOARD_THEME_KEY, null); if (klpThemeIdString != null) { if (sdkVersion <= VERSION_CODES.KITKAT) { try { final int themeId = Integer.parseInt(klpThemeIdString); - final KeyboardTheme theme = searchKeyboardThemeById(themeId); + final KeyboardTheme theme = searchKeyboardThemeById(themeId, + availableThemeArray); if (theme != null) { return theme; } @@ -125,25 +133,24 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> { prefs.edit().remove(KLP_KEYBOARD_THEME_KEY).apply(); } // TODO: This search algorithm isn't optimal if there are many themes. - for (final KeyboardTheme theme : KEYBOARD_THEMES) { + for (final KeyboardTheme theme : availableThemeArray) { if (sdkVersion >= theme.mMinApiVersion) { return theme; } } - return searchKeyboardThemeById(DEFAULT_THEME_ID); + return searchKeyboardThemeById(DEFAULT_THEME_ID, availableThemeArray); } public static String getKeyboardThemeName(final int themeId) { - final KeyboardTheme theme = searchKeyboardThemeById(themeId); + final KeyboardTheme theme = searchKeyboardThemeById(themeId, KEYBOARD_THEMES); return theme.mThemeName; } - public static void saveKeyboardThemeId(final String themeIdString, - final SharedPreferences prefs) { - saveKeyboardThemeId(themeIdString, prefs, BuildCompatUtils.EFFECTIVE_SDK_INT); + public static void saveKeyboardThemeId(final int themeId, final SharedPreferences prefs) { + saveKeyboardThemeId(themeId, prefs, BuildCompatUtils.EFFECTIVE_SDK_INT); } - @UsedForTesting + /* package private for testing */ static String getPreferenceKey(final int sdkVersion) { if (sdkVersion <= VERSION_CODES.KITKAT) { return KLP_KEYBOARD_THEME_KEY; @@ -151,26 +158,48 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> { return LXX_KEYBOARD_THEME_KEY; } - @UsedForTesting - static void saveKeyboardThemeId(final String themeIdString, - final SharedPreferences prefs, final int sdkVersion) { + /* package private for testing */ + static void saveKeyboardThemeId(final int themeId, final SharedPreferences prefs, + final int sdkVersion) { final String prefKey = getPreferenceKey(sdkVersion); - prefs.edit().putString(prefKey, themeIdString).apply(); + prefs.edit().putString(prefKey, Integer.toString(themeId)).apply(); + } + + public static KeyboardTheme getKeyboardTheme(final Context context) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final KeyboardTheme[] availableThemeArray = getAvailableThemeArray(context); + return getKeyboardTheme(prefs, BuildCompatUtils.EFFECTIVE_SDK_INT, availableThemeArray); } - public static KeyboardTheme getKeyboardTheme(final SharedPreferences prefs) { - return getKeyboardTheme(prefs, BuildCompatUtils.EFFECTIVE_SDK_INT); + /* package private for testing */ + static KeyboardTheme[] getAvailableThemeArray(final Context context) { + if (AVAILABLE_KEYBOARD_THEMES == null) { + final int[] availableThemeIdStringArray = context.getResources().getIntArray( + R.array.keyboard_theme_ids); + final ArrayList<KeyboardTheme> availableThemeList = new ArrayList<>(); + for (final int id : availableThemeIdStringArray) { + final KeyboardTheme theme = searchKeyboardThemeById(id, KEYBOARD_THEMES); + if (theme != null) { + availableThemeList.add(theme); + } + } + AVAILABLE_KEYBOARD_THEMES = availableThemeList.toArray( + new KeyboardTheme[availableThemeList.size()]); + Arrays.sort(AVAILABLE_KEYBOARD_THEMES); + } + return AVAILABLE_KEYBOARD_THEMES; } - @UsedForTesting - static KeyboardTheme getKeyboardTheme(final SharedPreferences prefs, final int sdkVersion) { + /* package private for testing */ + static KeyboardTheme getKeyboardTheme(final SharedPreferences prefs, final int sdkVersion, + final KeyboardTheme[] availableThemeArray) { final String lxxThemeIdString = prefs.getString(LXX_KEYBOARD_THEME_KEY, null); if (lxxThemeIdString == null) { - return getDefaultKeyboardTheme(prefs, sdkVersion); + return getDefaultKeyboardTheme(prefs, sdkVersion, availableThemeArray); } try { final int themeId = Integer.parseInt(lxxThemeIdString); - final KeyboardTheme theme = searchKeyboardThemeById(themeId); + final KeyboardTheme theme = searchKeyboardThemeById(themeId, availableThemeArray); if (theme != null) { return theme; } @@ -180,6 +209,6 @@ public final class KeyboardTheme implements Comparable<KeyboardTheme> { } // Remove preference that contains unknown or illegal theme id. prefs.edit().remove(LXX_KEYBOARD_THEME_KEY).apply(); - return getDefaultKeyboardTheme(prefs, sdkVersion); + return getDefaultKeyboardTheme(prefs, sdkVersion, availableThemeArray); } } diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 2b16785c2..06f9ced92 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -28,13 +28,13 @@ import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Typeface; import android.preference.PreferenceManager; +import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.accessibility.MainKeyboardAccessibilityDelegate; @@ -54,12 +54,14 @@ import com.android.inputmethod.keyboard.internal.SlidingKeyInputDrawingPreview; import com.android.inputmethod.keyboard.internal.TimerHandler; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.settings.DebugSettings; import com.android.inputmethod.latin.utils.CoordinateUtils; -import com.android.inputmethod.latin.utils.SpacebarLanguageUtils; +import com.android.inputmethod.latin.utils.StringUtils; import com.android.inputmethod.latin.utils.TypefaceUtils; +import java.util.Locale; import java.util.WeakHashMap; /** @@ -147,7 +149,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack // More keys keyboard 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<>(); @@ -674,7 +675,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack locatePreviewPlacerView(); panel.showInParent(mDrawingPreviewPlacerView); mMoreKeysPanel = panel; - dimEntireKeyboard(true /* dimmed */); } public boolean isShowingMoreKeysPanel() { @@ -688,7 +688,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack @Override public void onDismissMoreKeysPanel() { - dimEntireKeyboard(false /* dimmed */); if (isShowingMoreKeysPanel()) { mMoreKeysPanel.removeFromParent(); mMoreKeysPanel = null; @@ -816,24 +815,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack invalidateKey(mSpaceKey); } - private void dimEntireKeyboard(final boolean dimmed) { - final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed; - mNeedsToDimEntireKeyboard = dimmed; - if (needsRedrawing) { - invalidateAllKeys(); - } - } - - @Override - protected void onDraw(final Canvas canvas) { - super.onDraw(canvas); - - // Overlay a dark rectangle to dim. - if (mNeedsToDimEntireKeyboard) { - canvas.drawRect(0.0f, 0.0f, getWidth(), getHeight(), mBackgroundDimAlphaPaint); - } - } - @Override protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint, final KeyDrawParams params) { @@ -875,16 +856,26 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack // Layout language name on spacebar. private String layoutLanguageOnSpacebar(final Paint paint, - final InputMethodSubtype subtype, final int width) { + final RichInputMethodSubtype subtype, final int width) { + if (mLanguageOnSpacebarFormatType == LanguageOnSpacebarHelper.FORMAT_TYPE_MULTIPLE) { + final Locale[] locales = subtype.getLocales(); + final String[] languages = new String[locales.length]; + for (int i = 0; i < locales.length; ++i) { + languages[i] = StringUtils.toUpperCaseOfStringForLocale( + locales[i].getLanguage(), true /* needsToUpperCase */, Locale.ROOT); + } + return TextUtils.join(" / ", languages); + } + // Choose appropriate language name to fit into the width. if (mLanguageOnSpacebarFormatType == LanguageOnSpacebarHelper.FORMAT_TYPE_FULL_LOCALE) { - final String fullText = SpacebarLanguageUtils.getFullDisplayName(subtype); + final String fullText = subtype.getFullDisplayName(); if (fitsTextIntoWidth(width, fullText, paint)) { return fullText; } } - final String middleText = SpacebarLanguageUtils.getMiddleDisplayName(subtype); + final String middleText = subtype.getMiddleDisplayName(); if (fitsTextIntoWidth(width, middleText, paint)) { return middleText; } @@ -898,7 +889,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack paint.setTextAlign(Align.CENTER); paint.setTypeface(Typeface.DEFAULT); paint.setTextSize(mLanguageOnSpacebarTextSize); - final InputMethodSubtype subtype = getKeyboard().mId.mSubtype; + final RichInputMethodSubtype subtype = getKeyboard().mId.mSubtype; final String language = layoutLanguageOnSpacebar(paint, subtype, width); // Draw language text with shadow final float descent = paint.descent(); diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java index c19cd671a..9c5abcd71 100644 --- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java +++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java @@ -52,18 +52,11 @@ public class ProximityInfo { private final int mMostCommonKeyHeight; private final List<Key> mSortedKeys; private final List<Key>[] mGridNeighbors; - private final String mLocaleStr; @SuppressWarnings("unchecked") - ProximityInfo(final String localeStr, final int gridWidth, final int gridHeight, - final int minWidth, final int height, final int mostCommonKeyWidth, - final int mostCommonKeyHeight, final List<Key> sortedKeys, + ProximityInfo(final int gridWidth, final int gridHeight, final int minWidth, final int height, + final int mostCommonKeyWidth, final int mostCommonKeyHeight, final List<Key> sortedKeys, final TouchPositionCorrection touchPositionCorrection) { - if (TextUtils.isEmpty(localeStr)) { - mLocaleStr = ""; - } else { - mLocaleStr = localeStr; - } mGridWidth = gridWidth; mGridHeight = gridHeight; mGridSize = mGridWidth * mGridHeight; @@ -89,11 +82,10 @@ public class ProximityInfo { } // TODO: Stop passing proximityCharsArray - private static native long setProximityInfoNative(String locale, - int displayWidth, int displayHeight, int gridWidth, int gridHeight, - int mostCommonKeyWidth, int mostCommonKeyHeight, int[] proximityCharsArray, - int keyCount, int[] keyXCoordinates, int[] keyYCoordinates, int[] keyWidths, - int[] keyHeights, int[] keyCharCodes, float[] sweetSpotCenterXs, + private static native long setProximityInfoNative(int displayWidth, int displayHeight, + int gridWidth, int gridHeight, 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); @@ -221,10 +213,10 @@ public class ProximityInfo { } // TODO: Stop passing proximityCharsArray - return setProximityInfoNative(mLocaleStr, mKeyboardMinWidth, mKeyboardHeight, - mGridWidth, mGridHeight, mMostCommonKeyWidth, mMostCommonKeyHeight, - proximityCharsArray, keyCount, keyXCoordinates, keyYCoordinates, keyWidths, - keyHeights, keyCharCodes, sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii); + return setProximityInfoNative(mKeyboardMinWidth, mKeyboardHeight, 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/TextDecorator.java b/java/src/com/android/inputmethod/keyboard/TextDecorator.java index c22717f95..2b7d2ce3c 100644 --- a/java/src/com/android/inputmethod/keyboard/TextDecorator.java +++ b/java/src/com/android/inputmethod/keyboard/TextDecorator.java @@ -30,6 +30,7 @@ import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * A controller class of the add-to-dictionary indicator (a.k.a. TextDecorator). This class @@ -56,6 +57,7 @@ public class TextDecorator { private String mWaitingWord = null; private int mWaitingCursorStart = INVALID_CURSOR_INDEX; private int mWaitingCursorEnd = INVALID_CURSOR_INDEX; + @Nullable private CursorAnchorInfoCompatWrapper mCursorAnchorInfoWrapper = null; @Nonnull @@ -150,7 +152,7 @@ public class TextDecorator { * mode.</p> * @param info the compatibility wrapper object for the received {@link CursorAnchorInfo}. */ - public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) { + public void onUpdateCursorAnchorInfo(@Nullable final CursorAnchorInfoCompatWrapper info) { mCursorAnchorInfoWrapper = info; // Do not use layoutLater() to minimize the latency. layoutImmediately(); @@ -182,7 +184,7 @@ public class TextDecorator { private void layoutMain() { final CursorAnchorInfoCompatWrapper info = mCursorAnchorInfoWrapper; - if (info == null || !info.isAvailable()) { + if (info == null) { cancelLayoutInternalExpectedly("CursorAnchorInfo isn't available."); return; } @@ -301,7 +303,7 @@ public class TextDecorator { */ private static final class LayoutInvalidator { private final HandlerImpl mHandler; - public LayoutInvalidator(final TextDecorator ownerInstance) { + public LayoutInvalidator(@Nonnull final TextDecorator ownerInstance) { mHandler = new HandlerImpl(ownerInstance); } @@ -309,7 +311,7 @@ public class TextDecorator { private static final class HandlerImpl extends LeakGuardHandlerWrapper<TextDecorator> { - public HandlerImpl(final TextDecorator ownerInstance) { + public HandlerImpl(@Nonnull final TextDecorator ownerInstance) { super(ownerInstance); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java b/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java index df82becae..1a55359f5 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java +++ b/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java @@ -23,6 +23,8 @@ import com.android.inputmethod.keyboard.internal.DrawingHandler.Callbacks; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; +import javax.annotation.Nonnull; + // TODO: Separate this class into KeyPreviewHandler and BatchInputPreviewHandler or so. public class DrawingHandler extends LeakGuardHandlerWrapper<Callbacks> { public interface Callbacks { @@ -34,7 +36,7 @@ public class DrawingHandler extends LeakGuardHandlerWrapper<Callbacks> { private static final int MSG_DISMISS_KEY_PREVIEW = 0; private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; - public DrawingHandler(final Callbacks ownerInstance) { + public DrawingHandler(@Nonnull final Callbacks ownerInstance) { super(ownerInstance); } @@ -49,7 +51,7 @@ public class DrawingHandler extends LeakGuardHandlerWrapper<Callbacks> { callbacks.dismissKeyPreviewWithoutDelay((Key)msg.obj); break; case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT: - callbacks.showGestureFloatingPreviewText(SuggestedWords.EMPTY); + callbacks.showGestureFloatingPreviewText(SuggestedWords.getEmptyInstance()); break; } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java index fd84856b7..37ea0f17b 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java @@ -98,7 +98,7 @@ public class GestureFloatingTextDrawingPreview extends AbstractDrawingPreview { private final RectF mGesturePreviewRectangle = new RectF(); private int mPreviewTextX; private int mPreviewTextY; - private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; + private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance(); private final int[] mLastPointerCoords = CoordinateUtils.newInstance(); public GestureFloatingTextDrawingPreview(final TypedArray mainKeyboardViewAttr) { diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java index fa4192790..f4e010c4d 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -45,6 +45,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.Arrays; +import java.util.Locale; /** * Keyboard Building helper. @@ -162,6 +163,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> { mParams.mKeysCache = keysCache; } + public void setAllowRedundantMoreKes(final boolean enabled) { + mParams.mAllowRedundantMoreKeys = enabled; + } + public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) { mParams.mId = id; final XmlResourceParser parser = mResources.getXml(xmlId); @@ -277,7 +282,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> { params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0); params.mIconsSet.loadIcons(keyboardAttr); - params.mTextsSet.setLocale(params.mId.mLocale, mContext); + // TODO: this needs to be revisited for multi-lingual input. + params.mTextsSet.setLocale(params.mId.getLocales()[0], mContext); final int resourceId = keyboardAttr.getResourceId( R.styleable.Keyboard_touchPositionCorrectionData, 0); @@ -668,21 +674,22 @@ public class KeyboardBuilder<KP extends KeyboardParams> { R.styleable.Keyboard_Case_imeAction, id.imeAction()); final boolean isIconDefinedMatched = isIconDefined(caseAttr, R.styleable.Keyboard_Case_isIconDefined, mParams.mIconsSet); - final boolean localeCodeMatched = matchString(caseAttr, - R.styleable.Keyboard_Case_localeCode, id.mLocale.toString()); - final boolean languageCodeMatched = matchString(caseAttr, - R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage()); - final boolean countryCodeMatched = matchString(caseAttr, - R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry()); + final Locale[] locales = id.getLocales(); + final boolean localeCodeMatched = matchLocaleCodes(caseAttr, locales); + final boolean languageCodeMatched = matchLanguageCodes(caseAttr, locales); + final boolean countryCodeMatched = matchCountryCodes(caseAttr, locales); + final boolean splitLayoutMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_isSplitLayout, id.mIsSplitLayout); final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched && keyboardThemeMacthed && modeMatched && navigateNextMatched && navigatePreviousMatched && passwordInputMatched && clobberSettingsKeyMatched && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched && isMultiLineMatched && imeActionMatched && isIconDefinedMatched - && localeCodeMatched && languageCodeMatched && countryCodeMatched; + && localeCodeMatched && languageCodeMatched && countryCodeMatched + && splitLayoutMatched; if (DEBUG) { - startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, + startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE, textAttr(caseAttr.getString( R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"), textAttr(caseAttr.getString( @@ -707,6 +714,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> { "languageSwitchKeyEnabled"), booleanAttr(caseAttr, R.styleable.Keyboard_Case_isMultiLine, "isMultiLine"), + booleanAttr(caseAttr, R.styleable.Keyboard_Case_isSplitLayout, + "splitLayout"), textAttr(caseAttr.getString(R.styleable.Keyboard_Case_isIconDefined), "isIconDefined"), textAttr(caseAttr.getString(R.styleable.Keyboard_Case_localeCode), @@ -724,6 +733,23 @@ public class KeyboardBuilder<KP extends KeyboardParams> { } } + private boolean matchLocaleCodes(TypedArray caseAttr, final Locale[] locales) { + // TODO: adujst this for multilingual input + return matchString(caseAttr, R.styleable.Keyboard_Case_localeCode, locales[0].toString()); + } + + private boolean matchLanguageCodes(TypedArray caseAttr, Locale[] locales) { + // TODO: adujst this for multilingual input + return matchString(caseAttr, R.styleable.Keyboard_Case_languageCode, + locales[0].getLanguage()); + } + + private boolean matchCountryCodes(TypedArray caseAttr, Locale[] locales) { + // TODO: adujst this for multilingual input + return matchString(caseAttr, R.styleable.Keyboard_Case_countryCode, + locales[0].getCountry()); + } + private static boolean matchInteger(final TypedArray a, final int index, final int value) { // If <case> does not have "index" attribute, that means this <case> is wild-card for // the attribute. @@ -846,6 +872,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> { } private void endKeyboard() { + mParams.removeRedundantMoreKeys(); // {@link #parseGridRows(XmlPullParser,boolean)} may populate keyboard rows higher than // previously expected. final int actualHeight = mCurrentY - mParams.mVerticalGap + mParams.mBottomPadding; diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java index 5df9d3ece..71ce768a9 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java @@ -27,6 +27,9 @@ import java.util.Comparator; import java.util.SortedSet; import java.util.TreeSet; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + public class KeyboardParams { public KeyboardId mId; public int mThemeId; @@ -67,7 +70,8 @@ public class KeyboardParams { public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet(); public final KeyStylesSet mKeyStyles = new KeyStylesSet(mTextsSet); - public KeysCache mKeysCache; + @Nullable public KeysCache mKeysCache; + public boolean mAllowRedundantMoreKeys; public int mMostCommonKeyHeight = 0; public int mMostCommonKeyWidth = 0; @@ -95,7 +99,7 @@ public class KeyboardParams { clearHistogram(); } - public void onAddKey(final Key newKey) { + public void onAddKey(@Nonnull final Key newKey) { final Key key = (mKeysCache != null) ? mKeysCache.get(newKey) : newKey; final boolean isSpacer = key.isSpacer(); if (isSpacer && key.getWidth() == 0) { @@ -115,6 +119,26 @@ public class KeyboardParams { } } + public void removeRedundantMoreKeys() { + if (mAllowRedundantMoreKeys) { + return; + } + final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout = + new MoreKeySpec.LettersOnBaseLayout(); + for (final Key key : mSortedKeys) { + lettersOnBaseLayout.addLetter(key); + } + final ArrayList<Key> allKeys = new ArrayList<>(mSortedKeys); + mSortedKeys.clear(); + for (final Key key : allKeys) { + final Key filteredKey = Key.removeRedundantMoreKeys(key, lettersOnBaseLayout); + if (mKeysCache != null) { + mKeysCache.replace(key, filteredKey); + } + mSortedKeys.add(filteredKey); + } + } + private int mMaxHeightCount = 0; private int mMaxWidthCount = 0; private final SparseIntArray mHeightHistogram = new SparseIntArray(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java index b98ced97c..5f4d55bdb 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java @@ -19,6 +19,7 @@ package com.android.inputmethod.keyboard.internal; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.event.Event; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.utils.RecapitalizeStatus; @@ -29,7 +30,7 @@ import com.android.inputmethod.latin.utils.RecapitalizeStatus; * * The input events are {@link #onLoadKeyboard(int, int)}, {@link #onSaveKeyboardState()}, * {@link #onPressKey(int,boolean,int,int)}, {@link #onReleaseKey(int,boolean,int,int)}, - * {@link #onCodeInput(int,int,int)}, {@link #onFinishSlidingInput(int,int)}, + * {@link #onEvent(Event,int,int)}, {@link #onFinishSlidingInput(int,int)}, * {@link #onUpdateShiftState(int,int)}, {@link #onResetKeyboardStateToAlphabet(int,int)}. * * The actions are {@link SwitchActions}'s methods. @@ -610,10 +611,11 @@ public final class KeyboardState { return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER; } - public void onCodeInput(final int code, final int currentAutoCapsState, + public void onEvent(final Event event, final int currentAutoCapsState, final int currentRecapitalizeState) { + final int code = event.isFunctionalKeyEvent() ? event.mKeyCode : event.mCodePoint; if (DEBUG_EVENT) { - Log.d(TAG, "onCodeInput: code=" + Constants.printableCode(code) + Log.d(TAG, "onEvent: code=" + Constants.printableCode(code) + " autoCaps=" + currentAutoCapsState + " " + this); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java index cd6abeed3..f9297ac27 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java @@ -25,49 +25,42 @@ import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.utils.RunInLocale; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; -import java.util.HashMap; import java.util.Locale; +// TODO: Make this an immutable class. public final class KeyboardTextsSet { public static final String PREFIX_TEXT = "!text/"; + private static final String PREFIX_RESOURCE = "!string/"; public static final String SWITCH_TO_ALPHA_KEY_LABEL = "keylabel_to_alpha"; private static final char BACKSLASH = Constants.CODE_BACKSLASH; - private static final int MAX_STRING_REFERENCE_INDIRECTION = 10; + private static final int MAX_REFERENCE_INDIRECTION = 10; + private Resources mResources; + private Locale mResourceLocale; + private String mResourcePackageName; private String[] mTextsTable; - // Resource name to text map. - private HashMap<String, String> mResourceNameToTextsMap = new HashMap<>(); public void setLocale(final Locale locale, final Context context) { - mTextsTable = KeyboardTextsTable.getTextsTable(locale); final Resources res = context.getResources(); - final int referenceId = context.getApplicationInfo().labelRes; - final String resourcePackageName = res.getResourcePackageName(referenceId); - final RunInLocale<Void> job = new RunInLocale<Void>() { - @Override - protected Void job(final Resources resource) { - loadStringResourcesInternal(res, RESOURCE_NAMES, resourcePackageName); - return null; - } - }; // Null means the current system locale. - job.runInLocale(res, - SubtypeLocaleUtils.NO_LANGUAGE.equals(locale.toString()) ? null : locale); + final String resourcePackageName = res.getResourcePackageName( + context.getApplicationInfo().labelRes); + setLocale(locale, res, resourcePackageName); } @UsedForTesting - void loadStringResourcesInternal(final Resources res, final String[] resourceNames, + public void setLocale(final Locale locale, final Resources res, final String resourcePackageName) { - for (final String resName : resourceNames) { - final int resId = res.getIdentifier(resName, "string", resourcePackageName); - mResourceNameToTextsMap.put(resName, res.getString(resId)); - } + mResources = res; + // Null means the current system locale. + mResourceLocale = SubtypeLocaleUtils.NO_LANGUAGE.equals(locale.toString()) ? null : locale; + mResourcePackageName = resourcePackageName; + mTextsTable = KeyboardTextsTable.getTextsTable(locale); } public String getText(final String name) { - final String text = mResourceNameToTextsMap.get(name); - return (text != null) ? text : KeyboardTextsTable.getText(name, mTextsTable); + return KeyboardTextsTable.getText(name, mTextsTable); } private static int searchTextNameEnd(final String text, final int start) { @@ -93,13 +86,14 @@ public final class KeyboardTextsSet { StringBuilder sb; do { level++; - if (level >= MAX_STRING_REFERENCE_INDIRECTION) { - throw new RuntimeException("Too many " + PREFIX_TEXT + "name indirection: " + text); + if (level >= MAX_REFERENCE_INDIRECTION) { + throw new RuntimeException("Too many " + PREFIX_TEXT + " or " + PREFIX_RESOURCE + + " reference indirection: " + text); } - final int prefixLen = PREFIX_TEXT.length(); + final int prefixLength = PREFIX_TEXT.length(); final int size = text.length(); - if (size < prefixLen) { + if (size < prefixLength) { break; } @@ -110,10 +104,12 @@ public final class KeyboardTextsSet { if (sb == null) { sb = new StringBuilder(text.substring(0, pos)); } - final int end = searchTextNameEnd(text, pos + prefixLen); - final String name = text.substring(pos + prefixLen, end); - sb.append(getText(name)); - pos = end - 1; + pos = expandReference(text, pos, PREFIX_TEXT, sb); + } else if (text.startsWith(PREFIX_RESOURCE, pos)) { + if (sb == null) { + sb = new StringBuilder(text.substring(0, pos)); + } + pos = expandReference(text, pos, PREFIX_RESOURCE, sb); } else if (c == BACKSLASH) { if (sb != null) { // Append both escape character and escaped character. @@ -132,18 +128,24 @@ public final class KeyboardTextsSet { return TextUtils.isEmpty(text) ? null : text; } - // These texts' name should be aligned with the @string/<name> in - // values*/strings-action-keys.xml. - static final String[] RESOURCE_NAMES = { - // Labels for action. - "label_go_key", - "label_send_key", - "label_next_key", - "label_done_key", - "label_search_key", - "label_previous_key", - // Other labels. - "label_pause_key", - "label_wait_key", - }; + private int expandReference(final String text, final int pos, final String prefix, + final StringBuilder sb) { + final int prefixLength = prefix.length(); + final int end = searchTextNameEnd(text, pos + prefixLength); + final String name = text.substring(pos + prefixLength, end); + if (prefix.equals(PREFIX_TEXT)) { + sb.append(getText(name)); + } else { // PREFIX_RESOURCE + final String resourcePackageName = mResourcePackageName; + final RunInLocale<String> getTextJob = new RunInLocale<String>() { + @Override + protected String job(final Resources res) { + final int resId = res.getIdentifier(name, "string", resourcePackageName); + return res.getString(resId); + } + }; + sb.append(getTextJob.runInLocale(mResources, mResourceLocale)); + } + return end - 1; + } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java index 31bc549ca..a81d7ea2a 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java @@ -83,24 +83,24 @@ public final class KeyboardTextsTable { private static final String[] NAMES = { // /* index:histogram */ "name", - /* 0:32 */ "morekeys_a", - /* 1:32 */ "morekeys_o", - /* 2:30 */ "morekeys_u", - /* 3:30 */ "keylabel_to_alpha", - /* 4:29 */ "morekeys_e", - /* 5:28 */ "morekeys_i", - /* 6:23 */ "morekeys_c", - /* 7:23 */ "double_quotes", - /* 8:22 */ "morekeys_n", - /* 9:22 */ "single_quotes", - /* 10:20 */ "morekeys_s", - /* 11:17 */ "keyspec_currency", - /* 12:14 */ "morekeys_y", - /* 13:13 */ "morekeys_d", - /* 14:12 */ "morekeys_z", + /* 0:33 */ "morekeys_a", + /* 1:33 */ "morekeys_o", + /* 2:32 */ "morekeys_e", + /* 3:31 */ "morekeys_u", + /* 4:31 */ "keylabel_to_alpha", + /* 5:30 */ "morekeys_i", + /* 6:25 */ "morekeys_n", + /* 7:25 */ "morekeys_c", + /* 8:23 */ "double_quotes", + /* 9:22 */ "morekeys_s", + /* 10:22 */ "single_quotes", + /* 11:19 */ "keyspec_currency", + /* 12:17 */ "morekeys_y", + /* 13:16 */ "morekeys_z", + /* 14:14 */ "morekeys_d", /* 15:10 */ "morekeys_t", /* 16:10 */ "morekeys_l", - /* 17: 9 */ "morekeys_g", + /* 17:10 */ "morekeys_g", /* 18: 9 */ "single_angle_quotes", /* 19: 9 */ "double_angle_quotes", /* 20: 8 */ "morekeys_r", @@ -136,121 +136,129 @@ public final class KeyboardTextsTable { /* 50: 5 */ "additional_morekeys_symbols_8", /* 51: 5 */ "additional_morekeys_symbols_9", /* 52: 5 */ "additional_morekeys_symbols_0", - /* 53: 4 */ "morekeys_nordic_row2_11", - /* 54: 4 */ "morekeys_punctuation", - /* 55: 4 */ "keyspec_tablet_comma", - /* 56: 3 */ "keyspec_swiss_row1_11", - /* 57: 3 */ "keyspec_swiss_row2_10", - /* 58: 3 */ "keyspec_swiss_row2_11", - /* 59: 3 */ "morekeys_swiss_row1_11", - /* 60: 3 */ "morekeys_swiss_row2_10", - /* 61: 3 */ "morekeys_swiss_row2_11", - /* 62: 3 */ "morekeys_star", - /* 63: 3 */ "keyspec_left_parenthesis", - /* 64: 3 */ "keyspec_right_parenthesis", - /* 65: 3 */ "keyspec_left_square_bracket", - /* 66: 3 */ "keyspec_right_square_bracket", - /* 67: 3 */ "keyspec_left_curly_bracket", - /* 68: 3 */ "keyspec_right_curly_bracket", - /* 69: 3 */ "keyspec_less_than", - /* 70: 3 */ "keyspec_greater_than", - /* 71: 3 */ "keyspec_less_than_equal", - /* 72: 3 */ "keyspec_greater_than_equal", - /* 73: 3 */ "keyspec_left_double_angle_quote", - /* 74: 3 */ "keyspec_right_double_angle_quote", - /* 75: 3 */ "keyspec_left_single_angle_quote", - /* 76: 3 */ "keyspec_right_single_angle_quote", - /* 77: 3 */ "keyspec_comma", - /* 78: 3 */ "morekeys_tablet_comma", - /* 79: 3 */ "keyhintlabel_period", - /* 80: 3 */ "morekeys_tablet_period", - /* 81: 3 */ "morekeys_question", - /* 82: 2 */ "morekeys_h", - /* 83: 2 */ "morekeys_w", - /* 84: 2 */ "morekeys_east_slavic_row2_2", - /* 85: 2 */ "morekeys_cyrillic_u", - /* 86: 2 */ "morekeys_cyrillic_en", - /* 87: 2 */ "morekeys_cyrillic_ghe", - /* 88: 2 */ "morekeys_cyrillic_o", - /* 89: 2 */ "morekeys_cyrillic_i", - /* 90: 2 */ "keyspec_south_slavic_row1_6", - /* 91: 2 */ "keyspec_south_slavic_row2_11", - /* 92: 2 */ "keyspec_south_slavic_row3_1", - /* 93: 2 */ "keyspec_south_slavic_row3_8", - /* 94: 2 */ "morekeys_tablet_punctuation", - /* 95: 2 */ "keyspec_spanish_row2_10", - /* 96: 2 */ "morekeys_bullet", - /* 97: 2 */ "morekeys_left_parenthesis", - /* 98: 2 */ "morekeys_right_parenthesis", - /* 99: 2 */ "morekeys_arabic_diacritics", - /* 100: 2 */ "keyhintlabel_tablet_comma", - /* 101: 2 */ "keyspec_period", - /* 102: 2 */ "morekeys_period", - /* 103: 2 */ "keyspec_tablet_period", + /* 53: 5 */ "morekeys_tablet_period", + /* 54: 4 */ "morekeys_nordic_row2_11", + /* 55: 4 */ "morekeys_punctuation", + /* 56: 4 */ "keyspec_tablet_comma", + /* 57: 4 */ "keyspec_period", + /* 58: 4 */ "morekeys_period", + /* 59: 4 */ "keyspec_tablet_period", + /* 60: 3 */ "keyspec_swiss_row1_11", + /* 61: 3 */ "keyspec_swiss_row2_10", + /* 62: 3 */ "keyspec_swiss_row2_11", + /* 63: 3 */ "morekeys_swiss_row1_11", + /* 64: 3 */ "morekeys_swiss_row2_10", + /* 65: 3 */ "morekeys_swiss_row2_11", + /* 66: 3 */ "morekeys_star", + /* 67: 3 */ "keyspec_left_parenthesis", + /* 68: 3 */ "keyspec_right_parenthesis", + /* 69: 3 */ "keyspec_left_square_bracket", + /* 70: 3 */ "keyspec_right_square_bracket", + /* 71: 3 */ "keyspec_left_curly_bracket", + /* 72: 3 */ "keyspec_right_curly_bracket", + /* 73: 3 */ "keyspec_less_than", + /* 74: 3 */ "keyspec_greater_than", + /* 75: 3 */ "keyspec_less_than_equal", + /* 76: 3 */ "keyspec_greater_than_equal", + /* 77: 3 */ "keyspec_left_double_angle_quote", + /* 78: 3 */ "keyspec_right_double_angle_quote", + /* 79: 3 */ "keyspec_left_single_angle_quote", + /* 80: 3 */ "keyspec_right_single_angle_quote", + /* 81: 3 */ "keyspec_comma", + /* 82: 3 */ "morekeys_tablet_comma", + /* 83: 3 */ "keyhintlabel_period", + /* 84: 3 */ "morekeys_question", + /* 85: 2 */ "morekeys_h", + /* 86: 2 */ "morekeys_w", + /* 87: 2 */ "morekeys_east_slavic_row2_2", + /* 88: 2 */ "morekeys_cyrillic_u", + /* 89: 2 */ "morekeys_cyrillic_en", + /* 90: 2 */ "morekeys_cyrillic_ghe", + /* 91: 2 */ "morekeys_cyrillic_o", + /* 92: 2 */ "morekeys_cyrillic_i", + /* 93: 2 */ "keyspec_south_slavic_row1_6", + /* 94: 2 */ "keyspec_south_slavic_row2_11", + /* 95: 2 */ "keyspec_south_slavic_row3_1", + /* 96: 2 */ "keyspec_south_slavic_row3_8", + /* 97: 2 */ "morekeys_tablet_punctuation", + /* 98: 2 */ "keyspec_spanish_row2_10", + /* 99: 2 */ "morekeys_bullet", + /* 100: 2 */ "morekeys_left_parenthesis", + /* 101: 2 */ "morekeys_right_parenthesis", + /* 102: 2 */ "morekeys_arabic_diacritics", + /* 103: 2 */ "keyhintlabel_tablet_comma", /* 104: 2 */ "keyhintlabel_tablet_period", /* 105: 2 */ "keyspec_symbols_question", /* 106: 2 */ "keyspec_symbols_semicolon", /* 107: 2 */ "keyspec_symbols_percent", /* 108: 2 */ "morekeys_symbols_semicolon", /* 109: 2 */ "morekeys_symbols_percent", - /* 110: 1 */ "morekeys_v", - /* 111: 1 */ "morekeys_j", - /* 112: 1 */ "morekeys_q", - /* 113: 1 */ "morekeys_x", - /* 114: 1 */ "keyspec_q", - /* 115: 1 */ "keyspec_w", - /* 116: 1 */ "keyspec_y", - /* 117: 1 */ "keyspec_x", - /* 118: 1 */ "morekeys_east_slavic_row2_11", - /* 119: 1 */ "morekeys_cyrillic_ka", - /* 120: 1 */ "morekeys_cyrillic_a", - /* 121: 1 */ "morekeys_currency_dollar", - /* 122: 1 */ "morekeys_plus", - /* 123: 1 */ "morekeys_less_than", - /* 124: 1 */ "morekeys_greater_than", - /* 125: 1 */ "morekeys_exclamation", - /* 126: 0 */ "morekeys_currency_generic", - /* 127: 0 */ "morekeys_symbols_1", - /* 128: 0 */ "morekeys_symbols_2", - /* 129: 0 */ "morekeys_symbols_3", - /* 130: 0 */ "morekeys_symbols_4", - /* 131: 0 */ "morekeys_symbols_5", - /* 132: 0 */ "morekeys_symbols_6", - /* 133: 0 */ "morekeys_symbols_7", - /* 134: 0 */ "morekeys_symbols_8", - /* 135: 0 */ "morekeys_symbols_9", - /* 136: 0 */ "morekeys_symbols_0", - /* 137: 0 */ "morekeys_am_pm", - /* 138: 0 */ "keyspec_settings", - /* 139: 0 */ "keyspec_shortcut", - /* 140: 0 */ "keyspec_action_next", - /* 141: 0 */ "keyspec_action_previous", - /* 142: 0 */ "keylabel_to_more_symbol", - /* 143: 0 */ "keylabel_tablet_to_more_symbol", - /* 144: 0 */ "keylabel_to_phone_numeric", - /* 145: 0 */ "keylabel_to_phone_symbols", - /* 146: 0 */ "keylabel_time_am", - /* 147: 0 */ "keylabel_time_pm", - /* 148: 0 */ "keyspec_popular_domain", - /* 149: 0 */ "morekeys_popular_domain", - /* 150: 0 */ "keyspecs_left_parenthesis_more_keys", - /* 151: 0 */ "keyspecs_right_parenthesis_more_keys", - /* 152: 0 */ "single_laqm_raqm", - /* 153: 0 */ "single_raqm_laqm", - /* 154: 0 */ "double_laqm_raqm", - /* 155: 0 */ "double_raqm_laqm", - /* 156: 0 */ "single_lqm_rqm", - /* 157: 0 */ "single_9qm_lqm", - /* 158: 0 */ "single_9qm_rqm", - /* 159: 0 */ "single_rqm_9qm", - /* 160: 0 */ "double_lqm_rqm", - /* 161: 0 */ "double_9qm_lqm", - /* 162: 0 */ "double_9qm_rqm", - /* 163: 0 */ "double_rqm_9qm", - /* 164: 0 */ "morekeys_single_quote", - /* 165: 0 */ "morekeys_double_quote", - /* 166: 0 */ "morekeys_tablet_double_quote", - /* 167: 0 */ "keyspec_emoji_action_key", + /* 110: 2 */ "label_go_key", + /* 111: 2 */ "label_send_key", + /* 112: 2 */ "label_next_key", + /* 113: 2 */ "label_done_key", + /* 114: 2 */ "label_search_key", + /* 115: 2 */ "label_previous_key", + /* 116: 2 */ "label_pause_key", + /* 117: 2 */ "label_wait_key", + /* 118: 1 */ "morekeys_v", + /* 119: 1 */ "morekeys_j", + /* 120: 1 */ "morekeys_q", + /* 121: 1 */ "morekeys_x", + /* 122: 1 */ "keyspec_q", + /* 123: 1 */ "keyspec_w", + /* 124: 1 */ "keyspec_y", + /* 125: 1 */ "keyspec_x", + /* 126: 1 */ "morekeys_east_slavic_row2_11", + /* 127: 1 */ "morekeys_cyrillic_ka", + /* 128: 1 */ "morekeys_cyrillic_a", + /* 129: 1 */ "morekeys_currency_dollar", + /* 130: 1 */ "morekeys_plus", + /* 131: 1 */ "morekeys_less_than", + /* 132: 1 */ "morekeys_greater_than", + /* 133: 1 */ "morekeys_exclamation", + /* 134: 0 */ "morekeys_currency_generic", + /* 135: 0 */ "morekeys_symbols_1", + /* 136: 0 */ "morekeys_symbols_2", + /* 137: 0 */ "morekeys_symbols_3", + /* 138: 0 */ "morekeys_symbols_4", + /* 139: 0 */ "morekeys_symbols_5", + /* 140: 0 */ "morekeys_symbols_6", + /* 141: 0 */ "morekeys_symbols_7", + /* 142: 0 */ "morekeys_symbols_8", + /* 143: 0 */ "morekeys_symbols_9", + /* 144: 0 */ "morekeys_symbols_0", + /* 145: 0 */ "morekeys_am_pm", + /* 146: 0 */ "keyspec_settings", + /* 147: 0 */ "keyspec_shortcut", + /* 148: 0 */ "keyspec_action_next", + /* 149: 0 */ "keyspec_action_previous", + /* 150: 0 */ "keylabel_to_more_symbol", + /* 151: 0 */ "keylabel_tablet_to_more_symbol", + /* 152: 0 */ "keylabel_to_phone_numeric", + /* 153: 0 */ "keylabel_to_phone_symbols", + /* 154: 0 */ "keylabel_time_am", + /* 155: 0 */ "keylabel_time_pm", + /* 156: 0 */ "keyspec_popular_domain", + /* 157: 0 */ "morekeys_popular_domain", + /* 158: 0 */ "keyspecs_left_parenthesis_more_keys", + /* 159: 0 */ "keyspecs_right_parenthesis_more_keys", + /* 160: 0 */ "single_laqm_raqm", + /* 161: 0 */ "single_raqm_laqm", + /* 162: 0 */ "double_laqm_raqm", + /* 163: 0 */ "double_raqm_laqm", + /* 164: 0 */ "single_lqm_rqm", + /* 165: 0 */ "single_9qm_lqm", + /* 166: 0 */ "single_9qm_rqm", + /* 167: 0 */ "single_rqm_9qm", + /* 168: 0 */ "double_lqm_rqm", + /* 169: 0 */ "double_9qm_lqm", + /* 170: 0 */ "double_9qm_rqm", + /* 171: 0 */ "double_rqm_9qm", + /* 172: 0 */ "morekeys_single_quote", + /* 173: 0 */ "morekeys_double_quote", + /* 174: 0 */ "morekeys_tablet_double_quote", + /* 175: 0 */ "keyspec_emoji_action_key", }; private static final String EMPTY = ""; @@ -258,17 +266,16 @@ public final class KeyboardTextsTable { /* Default texts */ private static final String[] TEXTS_DEFAULT = { /* morekeys_a ~ */ - EMPTY, EMPTY, EMPTY, + EMPTY, EMPTY, EMPTY, EMPTY, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. /* keylabel_to_alpha */ "ABC", - /* morekeys_e ~ */ + /* morekeys_i ~ */ EMPTY, EMPTY, EMPTY, /* ~ morekeys_c */ /* double_quotes */ "!text/double_lqm_rqm", - /* morekeys_n */ EMPTY, - /* single_quotes */ "!text/single_lqm_rqm", /* morekeys_s */ EMPTY, + /* single_quotes */ "!text/single_lqm_rqm", /* keyspec_currency */ "$", /* morekeys_y ~ */ EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, @@ -291,10 +298,16 @@ public final class KeyboardTextsTable { // Label for "switch to symbols" key. /* keylabel_to_symbol */ "?123", /* additional_morekeys_symbols_1 ~ */ - EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - /* ~ morekeys_nordic_row2_11 */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ additional_morekeys_symbols_0 */ + /* morekeys_tablet_period */ "!text/morekeys_tablet_punctuation", + /* morekeys_nordic_row2_11 */ EMPTY, /* morekeys_punctuation */ "!autoColumnOrder!8,\\,,?,!,#,!text/keyspec_right_parenthesis,!text/keyspec_left_parenthesis,/,;,',@,:,-,\",+,\\%,&", /* keyspec_tablet_comma */ ",", + // Period key + /* keyspec_period */ ".", + /* morekeys_period */ "!text/morekeys_punctuation", + /* keyspec_tablet_period */ ".", /* keyspec_swiss_row1_11 ~ */ EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, /* ~ morekeys_swiss_row2_11 */ @@ -328,7 +341,6 @@ public final class KeyboardTextsTable { /* keyspec_comma */ ",", /* morekeys_tablet_comma */ EMPTY, /* keyhintlabel_period */ EMPTY, - /* morekeys_tablet_period */ "!text/morekeys_tablet_punctuation", // U+00BF: "¿" INVERTED QUESTION MARK /* morekeys_question */ "\u00BF", /* morekeys_h ~ */ @@ -345,19 +357,23 @@ public final class KeyboardTextsTable { /* morekeys_bullet */ "\u266A,\u2665,\u2660,\u2666,\u2663", /* morekeys_left_parenthesis */ "!fixedColumnOrder!3,!text/keyspecs_left_parenthesis_more_keys", /* morekeys_right_parenthesis */ "!fixedColumnOrder!3,!text/keyspecs_right_parenthesis_more_keys", - /* morekeys_arabic_diacritics */ EMPTY, - /* keyhintlabel_tablet_comma */ EMPTY, - // Period key - /* keyspec_period */ ".", - /* morekeys_period */ "!text/morekeys_punctuation", - /* keyspec_tablet_period */ ".", - /* keyhintlabel_tablet_period */ EMPTY, + /* morekeys_arabic_diacritics ~ */ + EMPTY, EMPTY, EMPTY, + /* ~ keyhintlabel_tablet_period */ /* keyspec_symbols_question */ "?", /* keyspec_symbols_semicolon */ ";", /* keyspec_symbols_percent */ "%", /* morekeys_symbols_semicolon */ EMPTY, // U+2030: "‰" PER MILLE SIGN /* morekeys_symbols_percent */ "\u2030", + /* label_go_key */ "!string/label_go_key", + /* label_send_key */ "!string/label_send_key", + /* label_next_key */ "!string/label_next_key", + /* label_done_key */ "!string/label_done_key", + /* label_search_key */ "!string/label_search_key", + /* label_previous_key */ "!string/label_previous_key", + /* label_pause_key */ "!string/label_pause_key", + /* label_wait_key */ "!string/label_wait_key", /* morekeys_v ~ */ EMPTY, EMPTY, EMPTY, EMPTY, /* ~ morekeys_x */ @@ -488,13 +504,6 @@ public final class KeyboardTextsTable { // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON /* morekeys_o */ "\u00F3,\u00F4,\u00F6,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B", - /* keylabel_to_alpha */ null, // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX @@ -503,6 +512,13 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B", + /* keylabel_to_alpha */ null, // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS @@ -511,13 +527,11 @@ public final class KeyboardTextsTable { // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON // U+0133: "ij" LATIN SMALL LIGATURE IJ /* morekeys_i */ "\u00ED,\u00EC,\u00EF,\u00EE,\u012F,\u012B,\u0133", - /* morekeys_c */ null, - /* double_quotes */ null, // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE /* morekeys_n */ "\u00F1,\u0144", - /* single_quotes ~ */ - null, null, null, + /* morekeys_c ~ */ + null, null, null, null, null, /* ~ keyspec_currency */ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE // U+0133: "ij" LATIN SMALL LIGATURE IJ @@ -527,7 +541,7 @@ public final class KeyboardTextsTable { /* Locale ar: Arabic */ private static final String[] TEXTS_ar = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0623: "أ" ARABIC LETTER ALEF WITH HAMZA ABOVE @@ -535,9 +549,9 @@ public final class KeyboardTextsTable { // U+0628: "ب" ARABIC LETTER BEH // U+062C: "ج" ARABIC LETTER JEEM /* keylabel_to_alpha */ "\u0623\u200C\u0628\u200C\u062C", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, /* ~ morekeys_cyrillic_soft_sign */ // U+0661: "١" ARABIC-INDIC DIGIT ONE /* keyspec_symbols_1 */ "\u0661", @@ -574,14 +588,17 @@ public final class KeyboardTextsTable { // U+066B: "٫" ARABIC DECIMAL SEPARATOR // U+066C: "٬" ARABIC THOUSANDS SEPARATOR /* additional_morekeys_symbols_0 */ "0,\u066B,\u066C", + /* morekeys_tablet_period */ "!text/morekeys_arabic_diacritics", /* morekeys_nordic_row2_11 */ null, /* morekeys_punctuation */ null, // U+061F: "؟" ARABIC QUESTION MARK // U+060C: "،" ARABIC COMMA // U+061B: "؛" ARABIC SEMICOLON /* keyspec_tablet_comma */ "\u060C", - /* keyspec_swiss_row1_11 ~ */ - null, null, null, null, null, null, + /* keyspec_period */ null, + /* morekeys_period */ "!text/morekeys_arabic_diacritics", + /* keyspec_tablet_period ~ */ + null, null, null, null, null, null, null, /* ~ morekeys_swiss_row2_11 */ // U+2605: "★" BLACK STAR // U+066D: "٭" ARABIC FIVE POINTED STAR @@ -611,7 +628,6 @@ public final class KeyboardTextsTable { /* morekeys_tablet_comma */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,\",\'", // U+0651: "ّ" ARABIC SHADDA /* keyhintlabel_period */ "\u0651", - /* morekeys_tablet_period */ "!text/morekeys_arabic_diacritics", // U+00BF: "¿" INVERTED QUESTION MARK /* morekeys_question */ "?,\u00BF", /* morekeys_h ~ */ @@ -643,9 +659,6 @@ public final class KeyboardTextsTable { // Note: The space character is needed as a preceding letter to draw Arabic diacritics characters correctly. /* morekeys_arabic_diacritics */ "!fixedColumnOrder!7, \u0655|\u0655, \u0654|\u0654, \u0652|\u0652, \u064D|\u064D, \u064C|\u064C, \u064B|\u064B, \u0651|\u0651, \u0656|\u0656, \u0670|\u0670, \u0653|\u0653, \u0650|\u0650, \u064F|\u064F, \u064E|\u064E,\u0640\u0640\u0640|\u0640", /* keyhintlabel_tablet_comma */ "\u061F", - /* keyspec_period */ null, - /* morekeys_period */ "!text/morekeys_arabic_diacritics", - /* keyspec_tablet_period */ null, /* keyhintlabel_tablet_period */ "\u0651", /* keyspec_symbols_question */ "\u061F", /* keyspec_symbols_semicolon */ "\u061B", @@ -658,8 +671,11 @@ public final class KeyboardTextsTable { /* Locale az_AZ: Azerbaijani (Azerbaijan) */ private static final String[] TEXTS_az_AZ = { + // This is the same as Turkish // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - /* morekeys_a */ "\u00E2", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + /* morekeys_a */ "\u00E2,\u00E4,\u00E1", // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX // U+0153: "œ" LATIN SMALL LIGATURE OE @@ -669,6 +685,9 @@ public final class KeyboardTextsTable { // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON /* morekeys_o */ "\u00F6,\u00F4,\u0153,\u00F2,\u00F3,\u00F5,\u00F8,\u014D", + // U+0259: "ə" LATIN SMALL LETTER SCHWA + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + /* morekeys_e */ "\u0259,\u00E9", // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE @@ -676,8 +695,6 @@ public final class KeyboardTextsTable { // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON /* morekeys_u */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", /* keylabel_to_alpha */ null, - // U+0259: "ə" LATIN SMALL LETTER SCHWA - /* morekeys_e */ "\u0259", // U+0131: "ı" LATIN SMALL LETTER DOTLESS I // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS @@ -686,20 +703,27 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u0131,\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* morekeys_n */ "\u0148,\u00F1", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE // U+010D: "č" LATIN SMALL LETTER C WITH CARON /* morekeys_c */ "\u00E7,\u0107,\u010D", - /* double_quotes ~ */ - null, null, null, - /* ~ single_quotes */ + /* double_quotes */ null, // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+0161: "š" LATIN SMALL LETTER S WITH CARON /* morekeys_s */ "\u015F,\u00DF,\u015B,\u0161", - /* keyspec_currency ~ */ - null, null, null, null, null, null, + /* single_quotes */ null, + /* keyspec_currency */ null, + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + /* morekeys_y */ "\u00FD", + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + /* morekeys_z */ "\u017E", + /* morekeys_d ~ */ + null, null, null, /* ~ morekeys_l */ // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE /* morekeys_g */ "\u011F", @@ -708,21 +732,21 @@ public final class KeyboardTextsTable { /* Locale be_BY: Belarusian (Belarus) */ private static final String[] TEXTS_be_BY = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0410: "А" CYRILLIC CAPITAL LETTER A // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* keylabel_to_alpha */ "\u0410\u0411\u0412", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, /* ~ morekeys_c */ /* double_quotes */ "!text/double_9qm_lqm", - /* morekeys_n */ null, + /* morekeys_s */ null, /* single_quotes */ "!text/single_9qm_lqm", - /* morekeys_s ~ */ - null, null, null, null, null, null, null, null, null, null, null, null, + /* keyspec_currency ~ */ + null, null, null, null, null, null, null, null, null, null, null, /* ~ morekeys_k */ // U+0451: "ё" CYRILLIC SMALL LETTER IO /* morekeys_cyrillic_ie */ "\u0451", @@ -744,33 +768,50 @@ public final class KeyboardTextsTable { /* Locale bg: Bulgarian */ private static final String[] TEXTS_bg = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0410: "А" CYRILLIC CAPITAL LETTER A // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* keylabel_to_alpha */ "\u0410\u0411\u0412", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, /* ~ morekeys_c */ // single_quotes of Bulgarian is default single_quotes_right_left. /* double_quotes */ "!text/double_9qm_lqm", }; + /* Locale bn_BD: Bengali (Bangladesh) */ + private static final String[] TEXTS_bn_BD = { + /* morekeys_a ~ */ + null, null, null, null, + /* ~ morekeys_u */ + // Label for "switch to alphabetic" key. + // U+0995: "क" BENGALI LETTER KA + // U+0996: "ख" BENGALI LETTER KHA + // U+0997: "ग" BENGALI LETTER GA + /* keylabel_to_alpha */ "\u0995\u0996\u0997", + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ + // U+09F3: "৳" BENGALI RUPEE SIGN + /* keyspec_currency */ "\u09F3", + }; + /* Locale bn_IN: Bengali (India) */ private static final String[] TEXTS_bn_IN = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0995: "क" BENGALI LETTER KA // U+0996: "ख" BENGALI LETTER KHA // U+0997: "ग" BENGALI LETTER GA /* keylabel_to_alpha */ "\u0995\u0996\u0997", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+20B9: "₹" INDIAN RUPEE SIGN /* keyspec_currency */ "\u20B9", }; @@ -798,13 +839,6 @@ public final class KeyboardTextsTable { // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON // U+00BA: "º" MASCULINE ORDINAL INDICATOR /* morekeys_o */ "\u00F2,\u00F3,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* keylabel_to_alpha */ null, // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS @@ -813,6 +847,13 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E8,\u00E9,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + /* keylabel_to_alpha */ null, // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE @@ -820,16 +861,15 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE // U+010D: "č" LATIN SMALL LETTER C WITH CARON /* morekeys_c */ "\u00E7,\u0107,\u010D", - /* double_quotes */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* morekeys_n */ "\u00F1,\u0144", - /* single_quotes ~ */ - null, null, null, null, null, null, null, + /* double_quotes ~ */ + null, null, null, null, null, null, null, null, /* ~ morekeys_t */ // U+00B7: "·" MIDDLE DOT // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE @@ -837,14 +877,14 @@ public final class KeyboardTextsTable { /* morekeys_g ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, /* ~ morekeys_nordic_row2_11 */ // U+00B7: "·" MIDDLE DOT /* morekeys_punctuation */ "!autoColumnOrder!9,\\,,?,!,\u00B7,#,),(,/,;,',@,:,-,\",+,\\%,&", /* keyspec_tablet_comma ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, /* ~ keyspec_south_slavic_row3_8 */ /* morekeys_tablet_punctuation */ "!autoColumnOrder!8,\\,,',\u00B7,#,),(,/,;,@,:,-,\",+,\\%,&", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA @@ -871,14 +911,6 @@ public final class KeyboardTextsTable { // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON /* morekeys_o */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B", - /* keylabel_to_alpha */ null, // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+011B: "ě" LATIN SMALL LETTER E WITH CARON // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE @@ -888,6 +920,14 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E9,\u011B,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B", + /* keylabel_to_alpha */ null, // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS @@ -895,30 +935,30 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00ED,\u00EE,\u00EF,\u00EC,\u012F,\u012B", + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u0148,\u00F1,\u0144", // U+010D: "č" LATIN SMALL LETTER C WITH CARON // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE /* morekeys_c */ "\u010D,\u00E7,\u0107", /* double_quotes */ "!text/double_9qm_lqm", - // U+0148: "ň" LATIN SMALL LETTER N WITH CARON - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* morekeys_n */ "\u0148,\u00F1,\u0144", - /* single_quotes */ "!text/single_9qm_lqm", // U+0161: "š" LATIN SMALL LETTER S WITH CARON // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE /* morekeys_s */ "\u0161,\u00DF,\u015B", + /* single_quotes */ "!text/single_9qm_lqm", /* keyspec_currency */ null, // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS /* morekeys_y */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* morekeys_d */ "\u010F", // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE /* morekeys_z */ "\u017E,\u017A,\u017C", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u010F", // U+0165: "ť" LATIN SMALL LETTER T WITH CARON /* morekeys_t */ "\u0165", /* morekeys_l */ null, @@ -931,20 +971,27 @@ public final class KeyboardTextsTable { /* Locale da: Danish */ private static final String[] TEXTS_da = { + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* morekeys_a */ "\u00E1,\u00E4,\u00E0,\u00E2,\u00E3,\u0101", + /* morekeys_a */ "\u00E5,\u00E6,\u00E1,\u00E4,\u00E0,\u00E2,\u00E3,\u0101", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE // U+0153: "œ" LATIN SMALL LIGATURE OE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* morekeys_o */ "\u00F3,\u00F4,\u00F2,\u00F5,\u0153,\u014D", + /* morekeys_o */ "\u00F8,\u00F6,\u00F3,\u00F4,\u00F2,\u00F5,\u0153,\u014D", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + /* morekeys_e */ "\u00E9,\u00EB", // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX @@ -952,29 +999,26 @@ public final class KeyboardTextsTable { // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON /* morekeys_u */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", /* keylabel_to_alpha */ null, - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - /* morekeys_e */ "\u00E9,\u00EB", // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS /* morekeys_i */ "\u00ED,\u00EF", - /* morekeys_c */ null, - /* double_quotes */ "!text/double_9qm_lqm", // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE /* morekeys_n */ "\u00F1,\u0144", - /* single_quotes */ "!text/single_9qm_lqm", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_lqm", // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+0161: "š" LATIN SMALL LETTER S WITH CARON /* morekeys_s */ "\u00DF,\u015B,\u0161", + /* single_quotes */ "!text/single_9qm_lqm", /* keyspec_currency */ null, // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS /* morekeys_y */ "\u00FD,\u00FF", + /* morekeys_z */ null, // U+00F0: "ð" LATIN SMALL LETTER ETH /* morekeys_d */ "\u00F0", - /* morekeys_z */ null, /* morekeys_t */ null, // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE /* morekeys_l */ "\u0142", @@ -994,8 +1038,8 @@ public final class KeyboardTextsTable { /* morekeys_nordic_row2_10 */ "\u00E4", /* keyspec_east_slavic_row1_9 ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, - /* ~ additional_morekeys_symbols_0 */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_tablet_period */ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS /* morekeys_nordic_row2_11 */ "\u00F6", }; @@ -1020,6 +1064,12 @@ public final class KeyboardTextsTable { // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON /* morekeys_o */ "\u00F6,%,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u00F8,\u014D", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0117", // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE @@ -1027,23 +1077,17 @@ public final class KeyboardTextsTable { // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON /* morekeys_u */ "\u00FC,%,\u00FB,\u00F9,\u00FA,\u016B", /* keylabel_to_alpha */ null, - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0117", /* morekeys_i */ null, - /* morekeys_c */ null, - /* double_quotes */ "!text/double_9qm_lqm", // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE /* morekeys_n */ "\u00F1,\u0144", - /* single_quotes */ "!text/single_9qm_lqm", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_lqm", // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+0161: "š" LATIN SMALL LETTER S WITH CARON /* morekeys_s */ "\u00DF,\u015B,\u0161", + /* single_quotes */ "!text/single_9qm_lqm", /* keyspec_currency ~ */ null, null, null, null, null, null, null, /* ~ morekeys_g */ @@ -1052,8 +1096,8 @@ public final class KeyboardTextsTable { /* morekeys_r ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, - /* ~ keyspec_tablet_comma */ + null, null, null, null, null, null, null, null, null, null, + /* ~ keyspec_tablet_period */ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS /* keyspec_swiss_row1_11 */ "\u00FC", // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS @@ -1071,7 +1115,7 @@ public final class KeyboardTextsTable { /* Locale el: Greek */ private static final String[] TEXTS_el = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0391: "Α" GREEK CAPITAL LETTER ALPHA @@ -1100,6 +1144,12 @@ public final class KeyboardTextsTable { // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE /* morekeys_o */ "\u00F3,\u00F4,\u00F6,\u00F2,\u0153,\u00F8,\u014D,\u00F5", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0113", // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS @@ -1107,24 +1157,17 @@ public final class KeyboardTextsTable { // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON /* morekeys_u */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B", /* keylabel_to_alpha */ null, - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0113", // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE /* morekeys_i */ "\u00ED,\u00EE,\u00EF,\u012B,\u00EC", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* morekeys_n */ "\u00F1", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA /* morekeys_c */ "\u00E7", /* double_quotes */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* morekeys_n */ "\u00F1", - /* single_quotes */ null, // U+00DF: "ß" LATIN SMALL LETTER SHARP S /* morekeys_s */ "\u00DF", }; @@ -1154,6 +1197,15 @@ public final class KeyboardTextsTable { // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE // U+00BA: "º" MASCULINE ORDINAL INDICATOR /* morekeys_o */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D,\u0151,\u00BA", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u011B,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX @@ -1166,15 +1218,6 @@ public final class KeyboardTextsTable { // U+00B5: "µ" MICRO SIGN /* morekeys_u */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B,\u0169,\u0171,\u0173,\u00B5", /* keylabel_to_alpha */ null, - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* morekeys_e */ "\u00E9,\u011B,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS @@ -1185,12 +1228,6 @@ public final class KeyboardTextsTable { // U+0131: "ı" LATIN SMALL LETTER DOTLESS I // U+0133: "ij" LATIN SMALL LIGATURE IJ /* morekeys_i */ "\u00ED,\u00EE,\u00EF,\u0129,\u00EC,\u012F,\u012B,\u0131,\u0133", - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE - /* morekeys_c */ "\u0107,\u010D,\u00E7,\u010B", - /* double_quotes */ null, // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA @@ -1198,27 +1235,33 @@ public final class KeyboardTextsTable { // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE // U+014B: "ŋ" LATIN SMALL LETTER ENG /* morekeys_n */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B", - /* single_quotes */ null, + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE + /* morekeys_c */ "\u0107,\u010D,\u00E7,\u010B", + /* double_quotes */ null, // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+0161: "š" LATIN SMALL LETTER S WITH CARON // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA /* morekeys_s */ "\u00DF,\u0161,\u015B,\u0219,\u015F", + /* single_quotes */ null, /* keyspec_currency */ null, // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE // U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS // U+00FE: "þ" LATIN SMALL LETTER THORN /* morekeys_y */ "y,\u00FD,\u0177,\u00FF,\u00FE", - // U+00F0: "ð" LATIN SMALL LETTER ETH - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - /* morekeys_d */ "\u00F0,\u010F,\u0111", // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON /* morekeys_z */ "\u017A,\u017C,\u017E", + // U+00F0: "ð" LATIN SMALL LETTER ETH + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + /* morekeys_d */ "\u00F0,\u010F,\u0111", // U+0165: "ť" LATIN SMALL LETTER T WITH CARON // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA @@ -1248,6 +1291,7 @@ public final class KeyboardTextsTable { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, /* ~ morekeys_question */ // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX // U+0127: "ħ" LATIN SMALL LETTER H WITH STROKE @@ -1260,8 +1304,9 @@ public final class KeyboardTextsTable { // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX /* keyspec_spanish_row2_10 */ "\u0135", /* morekeys_bullet ~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~ morekeys_symbols_percent */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, + /* ~ label_wait_key */ // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX /* morekeys_v */ "w,\u0175", /* morekeys_j */ null, @@ -1300,13 +1345,6 @@ public final class KeyboardTextsTable { // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON // U+00BA: "º" MASCULINE ORDINAL INDICATOR /* morekeys_o */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* keylabel_to_alpha */ null, // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS @@ -1315,6 +1353,13 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + /* keylabel_to_alpha */ null, // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE @@ -1322,18 +1367,18 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE // U+010D: "č" LATIN SMALL LETTER C WITH CARON /* morekeys_c */ "\u00E7,\u0107,\u010D", - /* double_quotes */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* morekeys_n */ "\u00F1,\u0144", - /* single_quotes ~ */ + /* double_quotes ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, /* ~ morekeys_nordic_row2_11 */ // U+00A1: "¡" INVERTED EXCLAMATION MARK // U+00BF: "¿" INVERTED QUESTION MARK @@ -1361,6 +1406,15 @@ public final class KeyboardTextsTable { // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE /* morekeys_o */ "\u00F6,\u00F5,\u00F2,\u00F3,\u00F4,\u0153,\u0151,\u00F8", + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + /* morekeys_e */ "\u0113,\u00E8,\u0117,\u00E9,\u00EA,\u00EB,\u0119,\u011B", // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK @@ -1371,15 +1425,6 @@ public final class KeyboardTextsTable { // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE /* morekeys_u */ "\u00FC,\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u016F,\u0171", /* keylabel_to_alpha */ null, - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - /* morekeys_e */ "\u0113,\u00E8,\u0117,\u00E9,\u00EA,\u00EB,\u0119,\u011B", // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK @@ -1388,31 +1433,31 @@ public final class KeyboardTextsTable { // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+0131: "ı" LATIN SMALL LETTER DOTLESS I /* morekeys_i */ "\u012B,\u00EC,\u012F,\u00ED,\u00EE,\u00EF,\u0131", + // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u0146,\u00F1,\u0144", // U+010D: "č" LATIN SMALL LETTER C WITH CARON // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE /* morekeys_c */ "\u010D,\u00E7,\u0107", /* double_quotes */ "!text/double_9qm_lqm", - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* morekeys_n */ "\u0146,\u00F1,\u0144", - /* single_quotes */ "!text/single_9qm_lqm", // U+0161: "š" LATIN SMALL LETTER S WITH CARON // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA /* morekeys_s */ "\u0161,\u00DF,\u015B,\u015F", + /* single_quotes */ "!text/single_9qm_lqm", /* keyspec_currency */ null, // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS /* morekeys_y */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* morekeys_d */ "\u010F", // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE /* morekeys_z */ "\u017E,\u017C,\u017A", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u010F", // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA // U+0165: "ť" LATIN SMALL LETTER T WITH CARON /* morekeys_t */ "\u0163,\u0165", @@ -1466,13 +1511,6 @@ public final class KeyboardTextsTable { // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON // U+00BA: "º" MASCULINE ORDINAL INDICATOR /* morekeys_o */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* keylabel_to_alpha */ null, // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS @@ -1481,6 +1519,13 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + /* keylabel_to_alpha */ null, // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE @@ -1488,20 +1533,19 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE // U+010D: "č" LATIN SMALL LETTER C WITH CARON /* morekeys_c */ "\u00E7,\u0107,\u010D", - /* double_quotes */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* morekeys_n */ "\u00F1,\u0144", }; /* Locale fa: Persian */ private static final String[] TEXTS_fa = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0627: "ا" ARABIC LETTER ALEF @@ -1509,9 +1553,9 @@ public final class KeyboardTextsTable { // U+0628: "ب" ARABIC LETTER BEH // U+067E: "پ" ARABIC LETTER PEH /* keylabel_to_alpha */ "\u0627\u200C\u0628\u200C\u067E", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+FDFC: "﷼" RIAL SIGN /* keyspec_currency */ "\uFDFC", /* morekeys_y ~ */ @@ -1553,6 +1597,7 @@ public final class KeyboardTextsTable { // U+066B: "٫" ARABIC DECIMAL SEPARATOR // U+066C: "٬" ARABIC THOUSANDS SEPARATOR /* additional_morekeys_symbols_0 */ "0,\u066B,\u066C", + /* morekeys_tablet_period */ "!text/morekeys_arabic_diacritics", /* morekeys_nordic_row2_11 */ null, /* morekeys_punctuation */ null, // U+060C: "،" ARABIC COMMA @@ -1561,8 +1606,10 @@ public final class KeyboardTextsTable { // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK /* keyspec_tablet_comma */ "\u060C", - /* keyspec_swiss_row1_11 ~ */ - null, null, null, null, null, null, + /* keyspec_period */ null, + /* morekeys_period */ "!text/morekeys_arabic_diacritics", + /* keyspec_tablet_period ~ */ + null, null, null, null, null, null, null, /* ~ morekeys_swiss_row2_11 */ // U+2605: "★" BLACK STAR // U+066D: "٭" ARABIC FIVE POINTED STAR @@ -1586,7 +1633,6 @@ public final class KeyboardTextsTable { /* morekeys_tablet_comma */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,!text/keyspec_left_double_angle_quote,!text/keyspec_right_double_angle_quote", // U+064B: "ً" ARABIC FATHATAN /* keyhintlabel_period */ "\u064B", - /* morekeys_tablet_period */ "!text/morekeys_arabic_diacritics", // U+00BF: "¿" INVERTED QUESTION MARK /* morekeys_question */ "?,\u00BF", /* morekeys_h ~ */ @@ -1618,9 +1664,6 @@ public final class KeyboardTextsTable { // Note: The space character is needed as a preceding letter to draw Arabic diacritics characters correctly. /* morekeys_arabic_diacritics */ "!fixedColumnOrder!7, \u0655|\u0655, \u0652|\u0652, \u0651|\u0651, \u064C|\u064C, \u064D|\u064D, \u064B|\u064B, \u0654|\u0654, \u0656|\u0656, \u0670|\u0670, \u0653|\u0653, \u064F|\u064F, \u0650|\u0650, \u064E|\u064E,\u0640\u0640\u0640|\u0640", /* keyhintlabel_tablet_comma */ "\u061F", - /* keyspec_period */ null, - /* morekeys_period */ "!text/morekeys_arabic_diacritics", - /* keyspec_tablet_period */ null, /* keyhintlabel_tablet_period */ "\u064B", /* keyspec_symbols_question */ "\u061F", /* keyspec_symbols_semicolon */ "\u061B", @@ -1629,8 +1672,9 @@ public final class KeyboardTextsTable { /* morekeys_symbols_semicolon */ ";", // U+2030: "‰" PER MILLE SIGN /* morekeys_symbols_percent */ "\\%,\u2030", - /* morekeys_v ~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, + /* label_go_key ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, /* ~ morekeys_plus */ // U+2264: "≤" LESS-THAN OR EQUAL TO // U+2265: "≥" GREATER-THAN EQUAL TO @@ -1644,13 +1688,16 @@ public final class KeyboardTextsTable { /* Locale fi: Finnish */ private static final String[] TEXTS_fi = { + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE // U+00E6: "æ" LATIN SMALL LETTER AE // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* morekeys_a */ "\u00E6,\u00E0,\u00E1,\u00E2,\u00E3,\u0101", + /* morekeys_a */ "\u00E4,\u00E5,\u00E6,\u00E0,\u00E1,\u00E2,\u00E3,\u0101", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE @@ -1658,25 +1705,26 @@ public final class KeyboardTextsTable { // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE // U+0153: "œ" LATIN SMALL LIGATURE OE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* morekeys_o */ "\u00F8,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u014D", + /* morekeys_o */ "\u00F6,\u00F8,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u014D", + /* morekeys_e */ null, // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS /* morekeys_u */ "\u00FC", /* keylabel_to_alpha ~ */ - null, null, null, null, null, null, null, - /* ~ single_quotes */ + null, null, null, null, null, + /* ~ double_quotes */ // U+0161: "š" LATIN SMALL LETTER S WITH CARON // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE /* morekeys_s */ "\u0161,\u00DF,\u015B", - /* keyspec_currency ~ */ + /* single_quotes ~ */ null, null, null, - /* ~ morekeys_d */ + /* ~ morekeys_y */ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE /* morekeys_z */ "\u017E,\u017A,\u017C", - /* morekeys_t ~ */ - null, null, null, null, null, null, null, null, + /* morekeys_d ~ */ + null, null, null, null, null, null, null, null, null, /* ~ morekeys_cyrillic_ie */ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE /* keyspec_nordic_row1_11 */ "\u00E5", @@ -1688,8 +1736,8 @@ public final class KeyboardTextsTable { /* morekeys_nordic_row2_10 */ "\u00F8", /* keyspec_east_slavic_row1_9 ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, - /* ~ additional_morekeys_symbols_0 */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_tablet_period */ // U+00E6: "æ" LATIN SMALL LETTER AE /* morekeys_nordic_row2_11 */ "\u00E6", }; @@ -1716,13 +1764,6 @@ public final class KeyboardTextsTable { // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON // U+00BA: "º" MASCULINE ORDINAL INDICATOR /* morekeys_o */ "\u00F4,\u0153,%,\u00F6,\u00F2,\u00F3,\u00F5,\u00F8,\u014D,\u00BA", - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00F9,\u00FB,%,\u00FC,\u00FA,\u016B", - /* keylabel_to_alpha */ null, // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX @@ -1731,6 +1772,13 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,%,\u0119,\u0117,\u0113", + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00F9,\u00FB,%,\u00FC,\u00FA,\u016B", + /* keylabel_to_alpha */ null, // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE @@ -1738,20 +1786,22 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00EE,%,\u00EF,\u00EC,\u00ED,\u012F,\u012B", + /* morekeys_n */ null, // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE // U+010D: "č" LATIN SMALL LETTER C WITH CARON /* morekeys_c */ "\u00E7,%,\u0107,\u010D", /* double_quotes ~ */ - null, null, null, null, null, + null, null, null, null, /* ~ keyspec_currency */ // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS /* morekeys_y */ "%,\u00FF", - /* morekeys_d ~ */ + /* morekeys_z ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~ keyspec_tablet_comma */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, + /* ~ keyspec_tablet_period */ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE /* keyspec_swiss_row1_11 */ "\u00E8", // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE @@ -1789,13 +1839,6 @@ public final class KeyboardTextsTable { // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON // U+00BA: "º" MASCULINE ORDINAL INDICATOR /* morekeys_o */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* keylabel_to_alpha */ null, // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS @@ -1804,6 +1847,13 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + /* keylabel_to_alpha */ null, // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE @@ -1811,29 +1861,28 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE // U+010D: "č" LATIN SMALL LETTER C WITH CARON /* morekeys_c */ "\u00E7,\u0107,\u010D", - /* double_quotes */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* morekeys_n */ "\u00F1,\u0144", }; /* Locale hi: Hindi */ private static final String[] TEXTS_hi = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0915: "क" DEVANAGARI LETTER KA // U+0916: "ख" DEVANAGARI LETTER KHA // U+0917: "ग" DEVANAGARI LETTER GA /* keylabel_to_alpha */ "\u0915\u0916\u0917", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+20B9: "₹" INDIAN RUPEE SIGN /* keyspec_currency */ "\u20B9", /* morekeys_y ~ */ @@ -1872,6 +1921,40 @@ public final class KeyboardTextsTable { /* additional_morekeys_symbols_8 */ "8", /* additional_morekeys_symbols_9 */ "9", /* additional_morekeys_symbols_0 */ "0", + /* morekeys_tablet_period */ "!autoColumnOrder!8,\\,,.,',#,),(,/,;,@,:,-,\",+,\\%,&", + /* morekeys_nordic_row2_11 ~ */ + null, null, null, + /* ~ keyspec_tablet_comma */ + // U+0964: "।" DEVANAGARI DANDA + /* keyspec_period */ "\u0964", + /* morekeys_period */ "!autoColumnOrder!9,\\,,.,?,!,#,),(,/,;,',@,:,-,\",+,\\%,&", + /* keyspec_tablet_period */ "\u0964", + }; + + /* Locale hi_ZZ: Hindi (ZZ) */ + private static final String[] TEXTS_hi_ZZ = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // U+20B9: "₹" INDIAN RUPEE SIGN + /* keyspec_currency */ "\u20B9", + /* morekeys_y ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, + /* ~ morekeys_symbols_percent */ + /* label_go_key */ "Go", + /* label_send_key */ "Send", + /* label_next_key */ "Next", + /* label_done_key */ "Done", + /* label_search_key */ "Search", + /* label_previous_key */ "Prev", + /* label_pause_key */ "Pause", + /* label_wait_key */ "Wait", }; /* Locale hr: Croatian */ @@ -1879,27 +1962,27 @@ public final class KeyboardTextsTable { /* morekeys_a ~ */ null, null, null, null, null, null, /* ~ morekeys_i */ + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", // U+010D: "č" LATIN SMALL LETTER C WITH CARON // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA /* morekeys_c */ "\u010D,\u0107,\u00E7", /* double_quotes */ "!text/double_9qm_rqm", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* morekeys_n */ "\u00F1,\u0144", - /* single_quotes */ "!text/single_9qm_rqm", // U+0161: "š" LATIN SMALL LETTER S WITH CARON // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+00DF: "ß" LATIN SMALL LETTER SHARP S /* morekeys_s */ "\u0161,\u015B,\u00DF", + /* single_quotes */ "!text/single_9qm_rqm", /* keyspec_currency */ null, /* morekeys_y */ null, - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - /* morekeys_d */ "\u0111", // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE /* morekeys_z */ "\u017E,\u017A,\u017C", + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + /* morekeys_d */ "\u0111", /* morekeys_t ~ */ null, null, null, /* ~ morekeys_g */ @@ -1928,14 +2011,6 @@ public final class KeyboardTextsTable { // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON /* morekeys_o */ "\u00F3,\u00F6,\u0151,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FA,\u00FC,\u0171,\u00FB,\u00F9,\u016B", - /* keylabel_to_alpha */ null, // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX @@ -1944,6 +2019,14 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u0171,\u00FB,\u00F9,\u016B", + /* keylabel_to_alpha */ null, // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS @@ -1951,12 +2034,13 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00ED,\u00EE,\u00EF,\u00EC,\u012F,\u012B", + /* morekeys_n */ null, /* morekeys_c */ null, /* double_quotes */ "!text/double_9qm_rqm", - /* morekeys_n */ null, + /* morekeys_s */ null, /* single_quotes */ "!text/single_9qm_rqm", - /* morekeys_s ~ */ - null, null, null, null, null, null, null, null, + /* keyspec_currency ~ */ + null, null, null, null, null, null, null, /* ~ morekeys_g */ /* single_angle_quotes */ "!text/single_raqm_laqm", /* double_angle_quotes */ "!text/double_raqm_laqm", @@ -1965,19 +2049,21 @@ public final class KeyboardTextsTable { /* Locale hy_AM: Armenian (Armenia) */ private static final String[] TEXTS_hy_AM = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0531: "Ա" ARMENIAN CAPITAL LETTER AYB // U+0532: "Բ" ARMENIAN CAPITAL LETTER BEN // U+0533: "Գ" ARMENIAN CAPITAL LETTER GIM /* keylabel_to_alpha */ "\u0531\u0532\u0533", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, - /* ~ morekeys_nordic_row2_11 */ + null, null, null, + /* ~ additional_morekeys_symbols_0 */ + /* morekeys_tablet_period */ "!text/morekeys_punctuation", + /* morekeys_nordic_row2_11 */ null, // U+055E: "՞" ARMENIAN QUESTION MARK // U+055C: "՜" ARMENIAN EXCLAMATION MARK // U+055A: "՚" ARMENIAN APOSTROPHE @@ -1990,6 +2076,10 @@ public final class KeyboardTextsTable { // U+055F: "՟" ARMENIAN ABBREVIATION MARK /* morekeys_punctuation */ "!autoColumnOrder!8,\\,,\u055E,\u055C,.,\u055A,\u0559,?,!,\u055D,\u055B,\u058A,\u00BB,\u00AB,\u055F,;,:", /* keyspec_tablet_comma */ "\u055D", + // U+0589: "։" ARMENIAN FULL STOP + /* keyspec_period */ "\u0589", + /* morekeys_period */ null, + /* keyspec_tablet_period */ "\u0589", /* keyspec_swiss_row1_11 ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, @@ -2002,21 +2092,14 @@ public final class KeyboardTextsTable { /* keyspec_comma */ "\u055D", /* morekeys_tablet_comma */ null, /* keyhintlabel_period */ null, - /* morekeys_tablet_period */ "!text/morekeys_punctuation", // U+055E: "՞" ARMENIAN QUESTION MARK // U+00BF: "¿" INVERTED QUESTION MARK /* morekeys_question */ "\u055E,\u00BF", /* morekeys_h ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, - /* ~ keyhintlabel_tablet_comma */ - // U+0589: "։" ARMENIAN FULL STOP - /* keyspec_period */ "\u0589", - /* morekeys_period */ null, - /* keyspec_tablet_period */ "\u0589", - /* keyhintlabel_tablet_period ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, /* ~ morekeys_greater_than */ // U+055C: "՜" ARMENIAN EXCLAMATION MARK // U+00A1: "¡" INVERTED EXCLAMATION MARK @@ -2043,13 +2126,6 @@ public final class KeyboardTextsTable { // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON /* morekeys_o */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", - /* keylabel_to_alpha */ null, // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE @@ -2058,6 +2134,13 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E9,\u00EB,\u00E8,\u00EA,\u0119,\u0117,\u0113", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", + /* keylabel_to_alpha */ null, // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX @@ -2065,18 +2148,18 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00ED,\u00EF,\u00EE,\u00EC,\u012F,\u012B", + /* morekeys_n */ null, /* morekeys_c */ null, /* double_quotes */ "!text/double_9qm_lqm", - /* morekeys_n */ null, - /* single_quotes */ "!text/single_9qm_lqm", /* morekeys_s */ null, + /* single_quotes */ "!text/single_9qm_lqm", /* keyspec_currency */ null, // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS /* morekeys_y */ "\u00FD,\u00FF", + /* morekeys_z */ null, // U+00F0: "ð" LATIN SMALL LETTER ETH /* morekeys_d */ "\u00F0", - /* morekeys_z */ null, // U+00FE: "þ" LATIN SMALL LETTER THORN /* morekeys_t */ "\u00FE", }; @@ -2103,13 +2186,6 @@ public final class KeyboardTextsTable { // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON // U+00BA: "º" MASCULINE ORDINAL INDICATOR /* morekeys_o */ "\u00F2,\u00F3,\u00F4,\u00F6,\u00F5,\u0153,\u00F8,\u014D,\u00BA", - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00F9,\u00FA,\u00FB,\u00FC,\u016B", - /* keylabel_to_alpha */ null, // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX @@ -2118,6 +2194,13 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0119,\u0117,\u0113", + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00F9,\u00FA,\u00FB,\u00FC,\u016B", + /* keylabel_to_alpha */ null, // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX @@ -2125,12 +2208,12 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00EC,\u00ED,\u00EE,\u00EF,\u012F,\u012B", - /* morekeys_c ~ */ + /* morekeys_n ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, - /* ~ keyspec_tablet_comma */ + null, null, null, null, null, null, null, null, null, + /* ~ keyspec_tablet_period */ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS /* keyspec_swiss_row1_11 */ "\u00FC", // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS @@ -2148,27 +2231,26 @@ public final class KeyboardTextsTable { /* Locale iw: Hebrew */ private static final String[] TEXTS_iw = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+05D0: "א" HEBREW LETTER ALEF // U+05D1: "ב" HEBREW LETTER BET // U+05D2: "ג" HEBREW LETTER GIMEL /* keylabel_to_alpha */ "\u05D0\u05D1\u05D2", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, /* ~ morekeys_c */ /* double_quotes */ "!text/double_rqm_9qm", - /* morekeys_n */ null, - /* single_quotes */ "!text/single_rqm_9qm", /* morekeys_s */ null, + /* single_quotes */ "!text/single_rqm_9qm", // U+20AA: "₪" NEW SHEQEL SIGN /* keyspec_currency */ "\u20AA", /* morekeys_y ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, + null, null, null, null, null, null, null, null, null, /* ~ morekeys_swiss_row2_11 */ // U+2605: "★" BLACK STAR /* morekeys_star */ "\u2605", @@ -2198,6 +2280,7 @@ public final class KeyboardTextsTable { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, /* ~ morekeys_currency_dollar */ // U+00B1: "±" PLUS-MINUS SIGN // U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN @@ -2207,34 +2290,34 @@ public final class KeyboardTextsTable { /* Locale ka_GE: Georgian (Georgia) */ private static final String[] TEXTS_ka_GE = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+10D0: "ა" GEORGIAN LETTER AN // U+10D1: "ბ" GEORGIAN LETTER BAN // U+10D2: "გ" GEORGIAN LETTER GAN /* keylabel_to_alpha */ "\u10D0\u10D1\u10D2", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, /* ~ morekeys_c */ /* double_quotes */ "!text/double_9qm_lqm", - /* morekeys_n */ null, + /* morekeys_s */ null, /* single_quotes */ "!text/single_9qm_lqm", }; /* Locale kk: Kazakh */ private static final String[] TEXTS_kk = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0410: "А" CYRILLIC CAPITAL LETTER A // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* keylabel_to_alpha */ "\u0410\u0411\u0412", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, + null, null, /* ~ morekeys_k */ // U+0451: "ё" CYRILLIC SMALL LETTER IO /* morekeys_cyrillic_ie */ "\u0451", @@ -2255,7 +2338,7 @@ public final class KeyboardTextsTable { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, /* ~ morekeys_w */ // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I /* morekeys_east_slavic_row2_2 */ "\u0456", @@ -2270,7 +2353,8 @@ public final class KeyboardTextsTable { /* morekeys_cyrillic_o */ "\u04E9", /* morekeys_cyrillic_i ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, /* ~ keyspec_x */ // U+04BB: "һ" CYRILLIC SMALL LETTER SHHA /* morekeys_east_slavic_row2_11 */ "\u04BB", @@ -2283,14 +2367,14 @@ public final class KeyboardTextsTable { /* Locale km_KH: Khmer (Cambodia) */ private static final String[] TEXTS_km_KH = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+1780: "ក" KHMER LETTER KA // U+1781: "ខ" KHMER LETTER KHA // U+1782: "គ" KHMER LETTER KO /* keylabel_to_alpha */ "\u1780\u1781\u1782", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, @@ -2298,7 +2382,8 @@ public final class KeyboardTextsTable { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, /* ~ morekeys_cyrillic_a */ // U+17DB: "៛" KHMER CURRENCY SYMBOL RIEL /* morekeys_currency_dollar */ "\u17DB,\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", @@ -2307,16 +2392,16 @@ public final class KeyboardTextsTable { /* Locale kn_IN: Kannada (India) */ private static final String[] TEXTS_kn_IN = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0C85: "ಅ" KANNADA LETTER A // U+0C86: "ಆ" KANNADA LETTER AA // U+0C87: "ಇ" KANNADA LETTER I /* keylabel_to_alpha */ "\u0C85\u0C86\u0C87", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+20B9: "₹" INDIAN RUPEE SIGN /* keyspec_currency */ "\u20B9", }; @@ -2324,16 +2409,16 @@ public final class KeyboardTextsTable { /* Locale ky: Kirghiz */ private static final String[] TEXTS_ky = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0410: "А" CYRILLIC CAPITAL LETTER A // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* keylabel_to_alpha */ "\u0410\u0411\u0412", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, + null, null, /* ~ morekeys_k */ // U+0451: "ё" CYRILLIC SMALL LETTER IO /* morekeys_cyrillic_ie */ "\u0451", @@ -2354,7 +2439,7 @@ public final class KeyboardTextsTable { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, /* ~ morekeys_east_slavic_row2_2 */ // U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U /* morekeys_cyrillic_u */ "\u04AF", @@ -2368,16 +2453,16 @@ public final class KeyboardTextsTable { /* Locale lo_LA: Lao (Laos) */ private static final String[] TEXTS_lo_LA = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0E81: "ກ" LAO LETTER KO // U+0E82: "ຂ" LAO LETTER KHO SUNG // U+0E84: "ຄ" LAO LETTER KHO TAM /* keylabel_to_alpha */ "\u0E81\u0E82\u0E84", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+20AD: "₭" KIP SIGN /* keyspec_currency */ "\u20AD", }; @@ -2403,6 +2488,15 @@ public final class KeyboardTextsTable { // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE /* morekeys_o */ "\u00F6,\u00F5,\u00F2,\u00F3,\u00F4,\u0153,\u0151,\u00F8", + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + /* morekeys_e */ "\u0117,\u0119,\u0113,\u00E8,\u00E9,\u00EA,\u00EB,\u011B", // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS @@ -2414,15 +2508,6 @@ public final class KeyboardTextsTable { // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE /* morekeys_u */ "\u016B,\u0173,\u00FC,\u016B,\u00F9,\u00FA,\u00FB,\u016F,\u0171", /* keylabel_to_alpha */ null, - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - /* morekeys_e */ "\u0117,\u0119,\u0113,\u00E8,\u00E9,\u00EA,\u00EB,\u011B", // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE @@ -2431,31 +2516,31 @@ public final class KeyboardTextsTable { // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+0131: "ı" LATIN SMALL LETTER DOTLESS I /* morekeys_i */ "\u012F,\u012B,\u00EC,\u00ED,\u00EE,\u00EF,\u0131", + // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u0146,\u00F1,\u0144", // U+010D: "č" LATIN SMALL LETTER C WITH CARON // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE /* morekeys_c */ "\u010D,\u00E7,\u0107", /* double_quotes */ "!text/double_9qm_lqm", - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* morekeys_n */ "\u0146,\u00F1,\u0144", - /* single_quotes */ "!text/single_9qm_lqm", // U+0161: "š" LATIN SMALL LETTER S WITH CARON // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA /* morekeys_s */ "\u0161,\u00DF,\u015B,\u015F", + /* single_quotes */ "!text/single_9qm_lqm", /* keyspec_currency */ null, // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS /* morekeys_y */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* morekeys_d */ "\u010F", // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE /* morekeys_z */ "\u017E,\u017C,\u017A", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u010F", // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA // U+0165: "ť" LATIN SMALL LETTER T WITH CARON /* morekeys_t */ "\u0163,\u0165", @@ -2498,6 +2583,15 @@ public final class KeyboardTextsTable { // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE /* morekeys_o */ "\u00F2,\u00F3,\u00F4,\u00F5,\u00F6,\u0153,\u0151,\u00F8", + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + /* morekeys_e */ "\u0113,\u0117,\u00E8,\u00E9,\u00EA,\u00EB,\u0119,\u011B", // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE @@ -2508,15 +2602,6 @@ public final class KeyboardTextsTable { // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE /* morekeys_u */ "\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u00FC,\u016F,\u0171", /* keylabel_to_alpha */ null, - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - /* morekeys_e */ "\u0113,\u0117,\u00E8,\u00E9,\u00EA,\u00EB,\u0119,\u011B", // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE @@ -2525,31 +2610,31 @@ public final class KeyboardTextsTable { // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+0131: "ı" LATIN SMALL LETTER DOTLESS I /* morekeys_i */ "\u012B,\u012F,\u00EC,\u00ED,\u00EE,\u00EF,\u0131", + // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u0146,\u00F1,\u0144", // U+010D: "č" LATIN SMALL LETTER C WITH CARON // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE /* morekeys_c */ "\u010D,\u00E7,\u0107", /* double_quotes */ "!text/double_9qm_lqm", - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* morekeys_n */ "\u0146,\u00F1,\u0144", - /* single_quotes */ "!text/single_9qm_lqm", // U+0161: "š" LATIN SMALL LETTER S WITH CARON // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA /* morekeys_s */ "\u0161,\u00DF,\u015B,\u015F", + /* single_quotes */ "!text/single_9qm_lqm", /* keyspec_currency */ null, // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS /* morekeys_y */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* morekeys_d */ "\u010F", // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE /* morekeys_z */ "\u017E,\u017C,\u017A", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u010F", // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA // U+0165: "ť" LATIN SMALL LETTER T WITH CARON /* morekeys_t */ "\u0163,\u0165", @@ -2574,21 +2659,21 @@ public final class KeyboardTextsTable { /* Locale mk: Macedonian */ private static final String[] TEXTS_mk = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0410: "А" CYRILLIC CAPITAL LETTER A // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* keylabel_to_alpha */ "\u0410\u0411\u0412", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, /* ~ morekeys_c */ /* double_quotes */ "!text/double_9qm_lqm", - /* morekeys_n */ null, + /* morekeys_s */ null, /* single_quotes */ "!text/single_9qm_lqm", - /* morekeys_s ~ */ - null, null, null, null, null, null, null, null, null, null, null, null, + /* keyspec_currency ~ */ + null, null, null, null, null, null, null, null, null, null, null, /* ~ morekeys_k */ // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE /* morekeys_cyrillic_ie */ "\u0450", @@ -2597,7 +2682,7 @@ public final class KeyboardTextsTable { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, /* ~ morekeys_cyrillic_o */ // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE /* morekeys_cyrillic_i */ "\u045D", @@ -2614,14 +2699,14 @@ public final class KeyboardTextsTable { /* Locale ml_IN: Malayalam (India) */ private static final String[] TEXTS_ml_IN = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0D05: "അ" MALAYALAM LETTER A /* keylabel_to_alpha */ "\u0D05", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+20B9: "₹" INDIAN RUPEE SIGN /* keyspec_currency */ "\u20B9", }; @@ -2629,16 +2714,16 @@ public final class KeyboardTextsTable { /* Locale mn_MN: Mongolian (Mongolia) */ private static final String[] TEXTS_mn_MN = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0410: "А" CYRILLIC CAPITAL LETTER A // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* keylabel_to_alpha */ "\u0410\u0411\u0412", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+20AE: "₮" TUGRIK SIGN /* keyspec_currency */ "\u20AE", }; @@ -2646,16 +2731,16 @@ public final class KeyboardTextsTable { /* Locale mr_IN: Marathi (India) */ private static final String[] TEXTS_mr_IN = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0915: "क" DEVANAGARI LETTER KA // U+0916: "ख" DEVANAGARI LETTER KHA // U+0917: "ग" DEVANAGARI LETTER GA /* keylabel_to_alpha */ "\u0915\u0916\u0917", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+20B9: "₹" INDIAN RUPEE SIGN /* keyspec_currency */ "\u20B9", /* morekeys_y ~ */ @@ -2699,14 +2784,14 @@ public final class KeyboardTextsTable { /* Locale my_MM: Burmese (Myanmar) */ private static final String[] TEXTS_my_MM = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+1000: "က" MYANMAR LETTER KA // U+1001: "ခ" MYANMAR LETTER KHA // U+1002: "ဂ" MYANMAR LETTER GA /* keylabel_to_alpha */ "\u1000\u1001\u1002", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, @@ -2716,48 +2801,41 @@ public final class KeyboardTextsTable { // U+104A: "၊" MYANMAR SIGN LITTLE SECTION // U+104B: "။" MYANMAR SIGN SECTION /* keyspec_tablet_comma */ "\u104A", + /* keyspec_period */ "\u104B", + /* morekeys_period */ null, + /* keyspec_tablet_period */ "\u104B", /* keyspec_swiss_row1_11 ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, /* ~ keyspec_comma */ /* morekeys_tablet_comma */ "\\,", /* keyhintlabel_period */ "\u104A", - /* morekeys_tablet_period ~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* morekeys_question ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, /* ~ keyspec_south_slavic_row3_8 */ /* morekeys_tablet_punctuation */ "!autoColumnOrder!8,.,',#,),(,/,;,@,...,:,-,\",+,\\%,&", - /* keyspec_spanish_row2_10 ~ */ - null, null, null, null, null, null, - /* ~ keyhintlabel_tablet_comma */ - /* keyspec_period */ "\u104B", - /* morekeys_period */ null, - /* keyspec_tablet_period */ "\u104B", }; /* Locale nb: Norwegian Bokmål */ private static final String[] TEXTS_nb = { - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* morekeys_a */ "\u00E0,\u00E4,\u00E1,\u00E2,\u00E3,\u0101", + /* morekeys_a */ "\u00E5,\u00E6,\u00E4,\u00E0,\u00E1,\u00E2,\u00E3,\u0101", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE // U+0153: "œ" LATIN SMALL LIGATURE OE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* morekeys_o */ "\u00F4,\u00F2,\u00F3,\u00F6,\u00F5,\u0153,\u014D", - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", - /* keylabel_to_alpha */ null, + /* morekeys_o */ "\u00F8,\u00F6,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u014D", // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX @@ -2766,13 +2844,20 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", - /* morekeys_i */ null, - /* morekeys_c */ null, + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", + /* keylabel_to_alpha ~ */ + null, null, null, null, + /* ~ morekeys_c */ /* double_quotes */ "!text/double_9qm_rqm", - /* morekeys_n */ null, + /* morekeys_s */ null, /* single_quotes */ "!text/single_9qm_rqm", - /* morekeys_s ~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, + /* keyspec_currency ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, /* ~ morekeys_cyrillic_ie */ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE /* keyspec_nordic_row1_11 */ "\u00E5", @@ -2784,8 +2869,8 @@ public final class KeyboardTextsTable { /* morekeys_nordic_row2_10 */ "\u00F6", /* keyspec_east_slavic_row1_9 ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, - /* ~ additional_morekeys_symbols_0 */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_tablet_period */ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS /* morekeys_nordic_row2_11 */ "\u00E4", }; @@ -2793,16 +2878,16 @@ public final class KeyboardTextsTable { /* Locale ne_NP: Nepali (Nepal) */ private static final String[] TEXTS_ne_NP = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0915: "क" DEVANAGARI LETTER KA // U+0916: "ख" DEVANAGARI LETTER KHA // U+0917: "ग" DEVANAGARI LETTER GA /* keylabel_to_alpha */ "\u0915\u0916\u0917", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+0930/U+0941/U+002E "रु." NEPALESE RUPEE SIGN /* keyspec_currency */ "\u0930\u0941.", /* morekeys_y ~ */ @@ -2841,6 +2926,14 @@ public final class KeyboardTextsTable { /* additional_morekeys_symbols_8 */ "8", /* additional_morekeys_symbols_9 */ "9", /* additional_morekeys_symbols_0 */ "0", + /* morekeys_tablet_period */ "!autoColumnOrder!8,.,\\,,',#,),(,/,;,@,:,-,\",+,\\%,&", + /* morekeys_nordic_row2_11 ~ */ + null, null, null, + /* ~ keyspec_tablet_comma */ + // U+0964: "।" DEVANAGARI DANDA + /* keyspec_period */ "\u0964", + /* morekeys_period */ "!autoColumnOrder!9,.,\\,,?,!,#,),(,/,;,',@,:,-,\",+,\\%,&", + /* keyspec_tablet_period */ "\u0964", }; /* Locale nl: Dutch */ @@ -2863,13 +2956,6 @@ public final class KeyboardTextsTable { // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON /* morekeys_o */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", - /* keylabel_to_alpha */ null, // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX @@ -2878,6 +2964,13 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E9,\u00EB,\u00EA,\u00E8,\u0119,\u0117,\u0113", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", + /* keylabel_to_alpha */ null, // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE @@ -2886,13 +2979,13 @@ public final class KeyboardTextsTable { // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON // U+0133: "ij" LATIN SMALL LIGATURE IJ /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B,\u0133", - /* morekeys_c */ null, - /* double_quotes */ "!text/double_9qm_rqm", // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE /* morekeys_n */ "\u00F1,\u0144", - /* single_quotes */ "!text/single_9qm_rqm", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_rqm", /* morekeys_s */ null, + /* single_quotes */ "!text/single_9qm_rqm", /* keyspec_currency */ null, // U+0133: "ij" LATIN SMALL LIGATURE IJ /* morekeys_y */ "\u0133", @@ -2919,8 +3012,6 @@ public final class KeyboardTextsTable { // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON /* morekeys_o */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - /* morekeys_u */ null, - /* keylabel_to_alpha */ null, // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE @@ -2929,27 +3020,29 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u0119,\u00E8,\u00E9,\u00EA,\u00EB,\u0117,\u0113", - /* morekeys_i */ null, + /* morekeys_u ~ */ + null, null, null, + /* ~ morekeys_i */ + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* morekeys_n */ "\u0144,\u00F1", // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+010D: "č" LATIN SMALL LETTER C WITH CARON /* morekeys_c */ "\u0107,\u00E7,\u010D", /* double_quotes */ "!text/double_9qm_rqm", - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* morekeys_n */ "\u0144,\u00F1", - /* single_quotes */ "!text/single_9qm_rqm", // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+0161: "š" LATIN SMALL LETTER S WITH CARON /* morekeys_s */ "\u015B,\u00DF,\u0161", - /* keyspec_currency ~ */ - null, null, null, - /* ~ morekeys_d */ + /* single_quotes */ "!text/single_9qm_rqm", + /* keyspec_currency */ null, + /* morekeys_y */ null, // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON /* morekeys_z */ "\u017C,\u017A,\u017E", + /* morekeys_d */ null, /* morekeys_t */ null, // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE /* morekeys_l */ "\u0142", @@ -2976,13 +3069,6 @@ public final class KeyboardTextsTable { // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON // U+00BA: "º" MASCULINE ORDINAL INDICATOR /* morekeys_o */ "\u00F3,\u00F5,\u00F4,\u00F2,\u00F6,\u0153,\u00F8,\u014D,\u00BA", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* keylabel_to_alpha */ null, // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE @@ -2991,6 +3077,13 @@ public final class KeyboardTextsTable { // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS /* morekeys_e */ "\u00E9,\u00EA,\u00E8,\u0119,\u0117,\u0113,\u00EB", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + /* keylabel_to_alpha */ null, // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE @@ -2998,6 +3091,7 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00ED,\u00EE,\u00EC,\u00EF,\u012F,\u012B", + /* morekeys_n */ null, // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+010D: "č" LATIN SMALL LETTER C WITH CARON // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE @@ -3019,19 +3113,19 @@ public final class KeyboardTextsTable { /* Locale ro: Romanian */ private static final String[] TEXTS_ro = { + // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS // U+00E6: "æ" LATIN SMALL LETTER AE // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* morekeys_a */ "\u00E2,\u00E3,\u0103,\u00E0,\u00E1,\u00E4,\u00E6,\u00E5,\u0101", + /* morekeys_a */ "\u0103,\u00E2,\u00E3,\u00E0,\u00E1,\u00E4,\u00E6,\u00E5,\u0101", /* morekeys_o ~ */ null, null, null, null, - /* ~ morekeys_e */ + /* ~ keylabel_to_alpha */ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE @@ -3039,18 +3133,18 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", + /* morekeys_n */ null, /* morekeys_c */ null, /* double_quotes */ "!text/double_9qm_rqm", - /* morekeys_n */ null, - /* single_quotes */ "!text/single_9qm_rqm", // U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+0161: "š" LATIN SMALL LETTER S WITH CARON /* morekeys_s */ "\u0219,\u00DF,\u015B,\u0161", + /* single_quotes */ "!text/single_9qm_rqm", /* keyspec_currency ~ */ null, null, null, null, - /* ~ morekeys_z */ + /* ~ morekeys_d */ // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW /* morekeys_t */ "\u021B", }; @@ -3058,21 +3152,21 @@ public final class KeyboardTextsTable { /* Locale ru: Russian */ private static final String[] TEXTS_ru = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0410: "А" CYRILLIC CAPITAL LETTER A // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* keylabel_to_alpha */ "\u0410\u0411\u0412", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, /* ~ morekeys_c */ /* double_quotes */ "!text/double_9qm_lqm", - /* morekeys_n */ null, + /* morekeys_s */ null, /* single_quotes */ "!text/single_9qm_lqm", - /* morekeys_s ~ */ - null, null, null, null, null, null, null, null, null, null, null, null, + /* keyspec_currency ~ */ + null, null, null, null, null, null, null, null, null, null, null, /* ~ morekeys_k */ // U+0451: "ё" CYRILLIC SMALL LETTER IO /* morekeys_cyrillic_ie */ "\u0451", @@ -3094,15 +3188,15 @@ public final class KeyboardTextsTable { /* Locale si_LK: Sinhalese (Sri Lanka) */ private static final String[] TEXTS_si_LK = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0D85: "අ" SINHALA LETTER AYANNA // U+0D86: "ආ" SINHALA LETTER AAYANNA /* keylabel_to_alpha */ "\u0D85,\u0D86", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+0DBB/U+0DD4: "රු" SINHALA LETTER RAYANNA/SINHALA VOWEL SIGN KETTI PAA-PILLA /* keyspec_currency */ "\u0DBB\u0DD4", }; @@ -3128,6 +3222,15 @@ public final class KeyboardTextsTable { // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE /* morekeys_o */ "\u00F4,\u00F3,\u00F6,\u00F2,\u00F5,\u0153,\u0151,\u00F8", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + /* morekeys_e */ "\u00E9,\u011B,\u0113,\u0117,\u00E8,\u00EA,\u00EB,\u0119", // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS @@ -3138,15 +3241,6 @@ public final class KeyboardTextsTable { // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE /* morekeys_u */ "\u00FA,\u016F,\u00FC,\u016B,\u0173,\u00F9,\u00FB,\u0171", /* keylabel_to_alpha */ null, - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - /* morekeys_e */ "\u00E9,\u011B,\u0113,\u0117,\u00E8,\u00EA,\u00EB,\u0119", // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK @@ -3155,32 +3249,32 @@ public final class KeyboardTextsTable { // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+0131: "ı" LATIN SMALL LETTER DOTLESS I /* morekeys_i */ "\u00ED,\u012B,\u012F,\u00EC,\u00EE,\u00EF,\u0131", - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - /* morekeys_c */ "\u010D,\u00E7,\u0107", - /* double_quotes */ "!text/double_9qm_lqm", // U+0148: "ň" LATIN SMALL LETTER N WITH CARON // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE /* morekeys_n */ "\u0148,\u0146,\u00F1,\u0144", - /* single_quotes */ "!text/single_9qm_lqm", + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + /* morekeys_c */ "\u010D,\u00E7,\u0107", + /* double_quotes */ "!text/double_9qm_lqm", // U+0161: "š" LATIN SMALL LETTER S WITH CARON // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA /* morekeys_s */ "\u0161,\u00DF,\u015B,\u015F", + /* single_quotes */ "!text/single_9qm_lqm", /* keyspec_currency */ null, // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS /* morekeys_y */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* morekeys_d */ "\u010F", // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE /* morekeys_z */ "\u017E,\u017C,\u017A", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u010F", // U+0165: "ť" LATIN SMALL LETTER T WITH CARON // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA /* morekeys_t */ "\u0165,\u0163", @@ -3205,22 +3299,21 @@ public final class KeyboardTextsTable { /* Locale sl: Slovenian */ private static final String[] TEXTS_sl = { /* morekeys_a ~ */ - null, null, null, null, null, null, - /* ~ morekeys_i */ + null, null, null, null, null, null, null, + /* ~ morekeys_n */ // U+010D: "č" LATIN SMALL LETTER C WITH CARON // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE /* morekeys_c */ "\u010D,\u0107", /* double_quotes */ "!text/double_9qm_lqm", - /* morekeys_n */ null, - /* single_quotes */ "!text/single_9qm_lqm", // U+0161: "š" LATIN SMALL LETTER S WITH CARON /* morekeys_s */ "\u0161", + /* single_quotes */ "!text/single_9qm_lqm", /* keyspec_currency */ null, /* morekeys_y */ null, - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - /* morekeys_d */ "\u0111", // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON /* morekeys_z */ "\u017E", + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + /* morekeys_d */ "\u0111", /* morekeys_t ~ */ null, null, null, /* ~ morekeys_g */ @@ -3231,7 +3324,7 @@ public final class KeyboardTextsTable { /* Locale sr: Serbian */ private static final String[] TEXTS_sr = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // END: More keys definitions for Serbian (Cyrillic) // Label for "switch to alphabetic" key. @@ -3239,14 +3332,14 @@ public final class KeyboardTextsTable { // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* keylabel_to_alpha */ "\u0410\u0411\u0412", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, /* ~ morekeys_c */ /* double_quotes */ "!text/double_9qm_lqm", - /* morekeys_n */ null, + /* morekeys_s */ null, /* single_quotes */ "!text/single_9qm_lqm", - /* morekeys_s ~ */ - null, null, null, null, null, null, null, null, + /* keyspec_currency ~ */ + null, null, null, null, null, null, null, /* ~ morekeys_g */ /* single_angle_quotes */ "!text/single_raqm_laqm", /* double_angle_quotes */ "!text/double_raqm_laqm", @@ -3259,7 +3352,7 @@ public final class KeyboardTextsTable { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, /* ~ morekeys_cyrillic_o */ // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE /* morekeys_cyrillic_i */ "\u045D", @@ -3291,20 +3384,75 @@ public final class KeyboardTextsTable { /* keyspec_south_slavic_row3_8 */ "\u0452", }; + /* Locale sr_ZZ: Serbian (ZZ) */ + private static final String[] TEXTS_sr_ZZ = { + /* morekeys_a */ null, + /* morekeys_o */ null, + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + /* morekeys_e */ "\u00E8", + /* morekeys_u */ null, + /* keylabel_to_alpha */ null, + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + /* morekeys_i */ "\u00EC", + /* morekeys_n */ null, + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + /* morekeys_c */ "\u010D,\u0107,%", + /* double_quotes */ null, + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + /* morekeys_s */ "\u0161,%", + /* single_quotes ~ */ + null, null, null, + /* ~ morekeys_y */ + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + /* morekeys_z */ "\u017E,%", + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + /* morekeys_d */ "\u0111,%", + /* morekeys_t ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, + /* ~ morekeys_symbols_percent */ + /* label_go_key */ "Idi", + /* label_send_key */ "\u0160alji", + /* label_next_key */ "Sled", + /* label_done_key */ "Gotov", + /* label_search_key */ "Tra\u017Ei", + /* label_previous_key */ "Preth", + /* label_pause_key */ "Pauza", + /* label_wait_key */ "\u010Cekaj", + }; + /* Locale sv: Swedish */ private static final String[] TEXTS_sv = { + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E5: "å" LATIN SMALL LETTER A WITH RING + // U+00E6: "æ" LATIN SMALL LETTER AE // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - /* morekeys_a */ "\u00E1,\u00E0,\u00E2,\u0105,\u00E3", + /* morekeys_a */ "\u00E4,\u00E5,\u00E6,\u00E1,\u00E0,\u00E2,\u0105,\u00E3", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* morekeys_o */ "\u00F3,\u00F2,\u00F4,\u00F5,\u014D", + /* morekeys_o */ "\u00F6,\u00F8,\u0153,\u00F3,\u00F2,\u00F4,\u00F5,\u014D", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119", // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE @@ -3312,43 +3460,37 @@ public final class KeyboardTextsTable { // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON /* morekeys_u */ "\u00FC,\u00FA,\u00F9,\u00FB,\u016B", /* keylabel_to_alpha */ null, - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119", // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS /* morekeys_i */ "\u00ED,\u00EC,\u00EE,\u00EF", + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + /* morekeys_n */ "\u0144,\u00F1,\u0148", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE // U+010D: "č" LATIN SMALL LETTER C WITH CARON /* morekeys_c */ "\u00E7,\u0107,\u010D", /* double_quotes */ null, - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0148: "ň" LATIN SMALL LETTER N WITH CARON - /* morekeys_n */ "\u0144,\u00F1,\u0148", - /* single_quotes */ null, // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+0161: "š" LATIN SMALL LETTER S WITH CARON // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA // U+00DF: "ß" LATIN SMALL LETTER SHARP S /* morekeys_s */ "\u015B,\u0161,\u015F,\u00DF", + /* single_quotes */ null, /* keyspec_currency */ null, // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS /* morekeys_y */ "\u00FD,\u00FF", - // U+00F0: "ð" LATIN SMALL LETTER ETH - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* morekeys_d */ "\u00F0,\u010F", // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE /* morekeys_z */ "\u017A,\u017E,\u017C", + // U+00F0: "ð" LATIN SMALL LETTER ETH + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + /* morekeys_d */ "\u00F0,\u010F", // U+0165: "ť" LATIN SMALL LETTER T WITH CARON // U+00FE: "þ" LATIN SMALL LETTER THORN /* morekeys_t */ "\u0165,\u00FE", @@ -3372,8 +3514,8 @@ public final class KeyboardTextsTable { /* morekeys_nordic_row2_10 */ "\u00F8,\u0153", /* keyspec_east_slavic_row1_9 ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, - /* ~ additional_morekeys_symbols_0 */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_tablet_period */ // U+00E6: "æ" LATIN SMALL LETTER AE /* morekeys_nordic_row2_11 */ "\u00E6", }; @@ -3399,6 +3541,12 @@ public final class KeyboardTextsTable { // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE /* morekeys_o */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113", // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE @@ -3406,28 +3554,21 @@ public final class KeyboardTextsTable { // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON /* morekeys_u */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", /* keylabel_to_alpha */ null, - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* morekeys_e */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113", // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE /* morekeys_i */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* morekeys_n */ "\u00F1", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA /* morekeys_c */ "\u00E7", /* double_quotes */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* morekeys_n */ "\u00F1", - /* single_quotes */ null, // U+00DF: "ß" LATIN SMALL LETTER SHARP S /* morekeys_s */ "\u00DF", - /* keyspec_currency ~ */ - null, null, null, null, null, null, + /* single_quotes ~ */ + null, null, null, null, null, null, null, /* ~ morekeys_l */ /* morekeys_g */ "g\'", }; @@ -3435,16 +3576,16 @@ public final class KeyboardTextsTable { /* Locale ta_IN: Tamil (India) */ private static final String[] TEXTS_ta_IN = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0BA4: "த" TAMIL LETTER TA // U+0BAE/U+0BBF: "மி" TAMIL LETTER MA/TAMIL VOWEL SIGN I // U+0BB4/U+0BCD: "ழ்" TAMIL LETTER LLLA/TAMIL SIGN VIRAMA /* keylabel_to_alpha */ "\u0BA4\u0BAE\u0BBF\u0BB4\u0BCD", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+0BF9: "௹" TAMIL RUPEE SIGN /* keyspec_currency */ "\u0BF9", }; @@ -3452,16 +3593,16 @@ public final class KeyboardTextsTable { /* Locale ta_LK: Tamil (Sri Lanka) */ private static final String[] TEXTS_ta_LK = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0BA4: "த" TAMIL LETTER TA // U+0BAE/U+0BBF: "மி" TAMIL LETTER MA/TAMIL VOWEL SIGN I // U+0BB4/U+0BCD: "ழ்" TAMIL LETTER LLLA/TAMIL SIGN VIRAMA /* keylabel_to_alpha */ "\u0BA4\u0BAE\u0BBF\u0BB4\u0BCD", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+0DBB/U+0DD4: "රු" SINHALA LETTER RAYANNA/SINHALA VOWEL SIGN KETTI PAA-PILLA /* keyspec_currency */ "\u0DBB\u0DD4", }; @@ -3469,7 +3610,7 @@ public final class KeyboardTextsTable { /* Locale ta_SG: Tamil (Singapore) */ private static final String[] TEXTS_ta_SG = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0BA4: "த" TAMIL LETTER TA @@ -3481,16 +3622,16 @@ public final class KeyboardTextsTable { /* Locale te_IN: Telugu (India) */ private static final String[] TEXTS_te_IN = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0C05: "అ" TELUGU LETTER A // U+0C06: "ఆ" TELUGU LETTER AA // U+0C07: "ఇ" TELUGU LETTER I /* keylabel_to_alpha */ "\u0C05\u0C06\u0C07", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+20B9: "₹" INDIAN RUPEE SIGN /* keyspec_currency */ "\u20B9", }; @@ -3498,16 +3639,16 @@ public final class KeyboardTextsTable { /* Locale th: Thai */ private static final String[] TEXTS_th = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0E01: "ก" THAI CHARACTER KO KAI // U+0E02: "ข" THAI CHARACTER KHO KHAI // U+0E04: "ค" THAI CHARACTER KHO KHWAI /* keylabel_to_alpha */ "\u0E01\u0E02\u0E04", - /* morekeys_e ~ */ - null, null, null, null, null, null, null, - /* ~ morekeys_s */ + /* morekeys_i ~ */ + null, null, null, null, null, null, + /* ~ single_quotes */ // U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT /* keyspec_currency */ "\u0E3F", }; @@ -3535,13 +3676,6 @@ public final class KeyboardTextsTable { // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON // U+00BA: "º" MASCULINE ORDINAL INDICATOR /* morekeys_o */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA", - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* keylabel_to_alpha */ null, // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS @@ -3550,6 +3684,13 @@ public final class KeyboardTextsTable { // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON /* morekeys_e */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", + /* keylabel_to_alpha */ null, // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE @@ -3557,20 +3698,21 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE + /* morekeys_n */ "\u00F1,\u0144", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE // U+010D: "č" LATIN SMALL LETTER C WITH CARON /* morekeys_c */ "\u00E7,\u0107,\u010D", - /* double_quotes */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* morekeys_n */ "\u00F1,\u0144", }; /* Locale tr: Turkish */ private static final String[] TEXTS_tr = { // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - /* morekeys_a */ "\u00E2", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + /* morekeys_a */ "\u00E2,\u00E4,\u00E1", // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX // U+0153: "œ" LATIN SMALL LIGATURE OE @@ -3580,6 +3722,9 @@ public final class KeyboardTextsTable { // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON /* morekeys_o */ "\u00F6,\u00F4,\u0153,\u00F2,\u00F3,\u00F5,\u00F8,\u014D", + // U+0259: "ə" LATIN SMALL LETTER SCHWA + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + /* morekeys_e */ "\u0259,\u00E9", // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE @@ -3587,7 +3732,6 @@ public final class KeyboardTextsTable { // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON /* morekeys_u */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", /* keylabel_to_alpha */ null, - /* morekeys_e */ null, // U+0131: "ı" LATIN SMALL LETTER DOTLESS I // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS @@ -3596,20 +3740,27 @@ public final class KeyboardTextsTable { // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON /* morekeys_i */ "\u0131,\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* morekeys_n */ "\u0148,\u00F1", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE // U+010D: "č" LATIN SMALL LETTER C WITH CARON /* morekeys_c */ "\u00E7,\u0107,\u010D", - /* double_quotes ~ */ - null, null, null, - /* ~ single_quotes */ + /* double_quotes */ null, // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+0161: "š" LATIN SMALL LETTER S WITH CARON /* morekeys_s */ "\u015F,\u00DF,\u015B,\u0161", - /* keyspec_currency ~ */ - null, null, null, null, null, null, + /* single_quotes */ null, + /* keyspec_currency */ null, + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + /* morekeys_y */ "\u00FD", + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + /* morekeys_z */ "\u017E", + /* morekeys_d ~ */ + null, null, null, /* ~ morekeys_l */ // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE /* morekeys_g */ "\u011F", @@ -3618,20 +3769,19 @@ public final class KeyboardTextsTable { /* Locale uk: Ukrainian */ private static final String[] TEXTS_uk = { /* morekeys_a ~ */ - null, null, null, + null, null, null, null, /* ~ morekeys_u */ // Label for "switch to alphabetic" key. // U+0410: "А" CYRILLIC CAPITAL LETTER A // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* keylabel_to_alpha */ "\u0410\u0411\u0412", - /* morekeys_e ~ */ + /* morekeys_i ~ */ null, null, null, /* ~ morekeys_c */ /* double_quotes */ "!text/double_9qm_lqm", - /* morekeys_n */ null, - /* single_quotes */ "!text/single_9qm_lqm", /* morekeys_s */ null, + /* single_quotes */ "!text/single_9qm_lqm", // U+20B4: "₴" HRYVNIA SIGN /* keyspec_currency */ "\u20B4", /* morekeys_y ~ */ @@ -3651,7 +3801,7 @@ public final class KeyboardTextsTable { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, /* ~ morekeys_w */ // U+0457: "ї" CYRILLIC SMALL LETTER YI /* morekeys_east_slavic_row2_2 */ "\u0457", @@ -3661,6 +3811,66 @@ public final class KeyboardTextsTable { /* morekeys_cyrillic_ghe */ "\u0491", }; + /* Locale uz_UZ: Uzbek (Uzbekistan) */ + private static final String[] TEXTS_uz_UZ = { + // This is the same as Turkish + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + /* morekeys_a */ "\u00E2,\u00E4,\u00E1", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + /* morekeys_o */ "\u00F6,\u00F4,\u0153,\u00F2,\u00F3,\u00F5,\u00F8,\u014D", + // U+0259: "ə" LATIN SMALL LETTER SCHWA + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + /* morekeys_e */ "\u0259,\u00E9", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + /* morekeys_u */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", + /* keylabel_to_alpha */ null, + // U+0131: "ı" LATIN SMALL LETTER DOTLESS I + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u0131,\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* morekeys_n */ "\u0148,\u00F1", + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,\u0107,\u010D", + /* double_quotes */ null, + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + /* morekeys_s */ "\u015F,\u00DF,\u015B,\u0161", + /* single_quotes */ null, + /* keyspec_currency */ null, + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + /* morekeys_y */ "\u00FD", + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + /* morekeys_z */ "\u017E", + /* morekeys_d ~ */ + null, null, null, + /* ~ morekeys_l */ + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u011F", + }; + /* Locale vi: Vietnamese */ private static final String[] TEXTS_vi = { // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE @@ -3699,6 +3909,18 @@ public final class KeyboardTextsTable { // U+1EE1: "ỡ" LATIN SMALL LETTER O WITH HORN AND TILDE // U+1EE3: "ợ" LATIN SMALL LETTER O WITH HORN AND DOT BELOW /* morekeys_o */ "\u00F2,\u00F3,\u1ECF,\u00F5,\u1ECD,\u00F4,\u1ED3,\u1ED1,\u1ED5,\u1ED7,\u1ED9,\u01A1,\u1EDD,\u1EDB,\u1EDF,\u1EE1,\u1EE3", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+1EBB: "ẻ" LATIN SMALL LETTER E WITH HOOK ABOVE + // U+1EBD: "ẽ" LATIN SMALL LETTER E WITH TILDE + // U+1EB9: "ẹ" LATIN SMALL LETTER E WITH DOT BELOW + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+1EC1: "ề" LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE + // U+1EBF: "ế" LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE + // U+1EC3: "ể" LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE + // U+1EC5: "ễ" LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE + // U+1EC7: "ệ" LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW + /* morekeys_e */ "\u00E8,\u00E9,\u1EBB,\u1EBD,\u1EB9,\u00EA,\u1EC1,\u1EBF,\u1EC3,\u1EC5,\u1EC7", // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE // U+1EE7: "ủ" LATIN SMALL LETTER U WITH HOOK ABOVE @@ -3712,27 +3934,15 @@ public final class KeyboardTextsTable { // U+1EF1: "ự" LATIN SMALL LETTER U WITH HORN AND DOT BELOW /* morekeys_u */ "\u00F9,\u00FA,\u1EE7,\u0169,\u1EE5,\u01B0,\u1EEB,\u1EE9,\u1EED,\u1EEF,\u1EF1", /* keylabel_to_alpha */ null, - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+1EBB: "ẻ" LATIN SMALL LETTER E WITH HOOK ABOVE - // U+1EBD: "ẽ" LATIN SMALL LETTER E WITH TILDE - // U+1EB9: "ẹ" LATIN SMALL LETTER E WITH DOT BELOW - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+1EC1: "ề" LATIN SMALL LETTER E WITH CIRCUMFLEX AND GRAVE - // U+1EBF: "ế" LATIN SMALL LETTER E WITH CIRCUMFLEX AND ACUTE - // U+1EC3: "ể" LATIN SMALL LETTER E WITH CIRCUMFLEX AND HOOK ABOVE - // U+1EC5: "ễ" LATIN SMALL LETTER E WITH CIRCUMFLEX AND TILDE - // U+1EC7: "ệ" LATIN SMALL LETTER E WITH CIRCUMFLEX AND DOT BELOW - /* morekeys_e */ "\u00E8,\u00E9,\u1EBB,\u1EBD,\u1EB9,\u00EA,\u1EC1,\u1EBF,\u1EC3,\u1EC5,\u1EC7", // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+1EC9: "ỉ" LATIN SMALL LETTER I WITH HOOK ABOVE // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE // U+1ECB: "ị" LATIN SMALL LETTER I WITH DOT BELOW /* morekeys_i */ "\u00EC,\u00ED,\u1EC9,\u0129,\u1ECB", - /* morekeys_c ~ */ + /* morekeys_n ~ */ null, null, null, null, null, - /* ~ morekeys_s */ + /* ~ single_quotes */ // U+20AB: "₫" DONG SIGN /* keyspec_currency */ "\u20AB", // U+1EF3: "ỳ" LATIN SMALL LETTER Y WITH GRAVE @@ -3741,6 +3951,7 @@ public final class KeyboardTextsTable { // U+1EF9: "ỹ" LATIN SMALL LETTER Y WITH TILDE // U+1EF5: "ỵ" LATIN SMALL LETTER Y WITH DOT BELOW /* morekeys_y */ "\u1EF3,\u00FD,\u1EF7,\u1EF9,\u1EF5", + /* morekeys_z */ null, // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE /* morekeys_d */ "\u0111", }; @@ -3766,6 +3977,12 @@ public final class KeyboardTextsTable { // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE /* morekeys_o */ "\u00F3,\u00F4,\u00F6,\u00F2,\u0153,\u00F8,\u014D,\u00F5", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0113", // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS @@ -3773,24 +3990,17 @@ public final class KeyboardTextsTable { // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON /* morekeys_u */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B", /* keylabel_to_alpha */ null, - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* morekeys_e */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0113", // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE /* morekeys_i */ "\u00ED,\u00EE,\u00EF,\u012B,\u00EC", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* morekeys_n */ "\u00F1", // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA /* morekeys_c */ "\u00E7", /* double_quotes */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* morekeys_n */ "\u00F1", - /* single_quotes */ null, // U+00DF: "ß" LATIN SMALL LETTER SHARP S /* morekeys_s */ "\u00DF", }; @@ -3821,6 +4031,16 @@ public final class KeyboardTextsTable { // U+0153: "œ" LATIN SMALL LIGATURE OE // U+00BA: "º" MASCULINE ORDINAL INDICATOR /* morekeys_o */ "\u00F2,\u00F3,\u00F4,\u00F5,\u00F6,\u00F8,\u014D,\u014F,\u0151,\u0153,\u00BA", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON + // U+0115: "ĕ" LATIN SMALL LETTER E WITH BREVE + // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // U+011B: "ě" LATIN SMALL LETTER E WITH CARON + /* morekeys_e */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113,\u0115,\u0117,\u0119,\u011B", // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX @@ -3833,16 +4053,6 @@ public final class KeyboardTextsTable { // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK /* morekeys_u */ "\u00F9,\u00FA,\u00FB,\u00FC,\u0169,\u016B,\u016D,\u016F,\u0171,\u0173", /* keylabel_to_alpha */ null, - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - // U+0115: "ĕ" LATIN SMALL LETTER E WITH BREVE - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // U+011B: "ě" LATIN SMALL LETTER E WITH CARON - /* morekeys_e */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113,\u0115,\u0117,\u0119,\u011B", // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX @@ -3854,13 +4064,6 @@ public final class KeyboardTextsTable { // U+0131: "ı" LATIN SMALL LETTER DOTLESS I // U+0133: "ij" LATIN SMALL LIGATURE IJ /* morekeys_i */ "\u00EC,\u00ED,\u00EE,\u00EF,\u0129,\u012B,\u012D,\u012F,\u0131,\u0133", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX - // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - /* morekeys_c */ "\u00E7,\u0107,\u0109,\u010B,\u010D", - /* double_quotes */ null, // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA @@ -3868,7 +4071,13 @@ public final class KeyboardTextsTable { // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE // U+014B: "ŋ" LATIN SMALL LETTER ENG /* morekeys_n */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B", - /* single_quotes */ null, + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX + // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + /* morekeys_c */ "\u00E7,\u0107,\u0109,\u010B,\u010D", + /* double_quotes */ null, // U+00DF: "ß" LATIN SMALL LETTER SHARP S // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX @@ -3876,20 +4085,21 @@ public final class KeyboardTextsTable { // U+0161: "š" LATIN SMALL LETTER S WITH CARON // U+017F: "ſ" LATIN SMALL LETTER LONG S /* morekeys_s */ "\u00DF,\u015B,\u015D,\u015F,\u0161,\u017F", + /* single_quotes */ null, /* keyspec_currency */ null, // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE // U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS // U+0133: "ij" LATIN SMALL LIGATURE IJ /* morekeys_y */ "\u00FD,\u0177,\u00FF,\u0133", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - // U+00F0: "ð" LATIN SMALL LETTER ETH - /* morekeys_d */ "\u010F,\u0111,\u00F0", // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON /* morekeys_z */ "\u017A,\u017C,\u017E", + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + // U+00F0: "ð" LATIN SMALL LETTER ETH + /* morekeys_d */ "\u010F,\u0111,\u00F0", // U+00FE: "þ" LATIN SMALL LETTER THORN // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA // U+0165: "ť" LATIN SMALL LETTER T WITH CARON @@ -3920,6 +4130,7 @@ public final class KeyboardTextsTable { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, /* ~ morekeys_question */ // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX /* morekeys_h */ "\u0125", @@ -3927,7 +4138,8 @@ public final class KeyboardTextsTable { /* morekeys_w */ "\u0175", /* morekeys_east_slavic_row2_2 ~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, /* ~ morekeys_v */ // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX /* morekeys_j */ "\u0135", @@ -3935,72 +4147,76 @@ public final class KeyboardTextsTable { private static final Object[] LOCALES_AND_TEXTS = { // "locale", TEXT_ARRAY, /* numberOfNonNullText/lengthOf_TEXT_ARRAY localeName */ - "DEFAULT", TEXTS_DEFAULT, /* 168/168 DEFAULT */ + "DEFAULT", TEXTS_DEFAULT, /* 176/176 DEFAULT */ "af" , TEXTS_af, /* 7/ 13 Afrikaans */ "ar" , TEXTS_ar, /* 55/110 Arabic */ - "az_AZ" , TEXTS_az_AZ, /* 8/ 18 Azerbaijani (Azerbaijan) */ + "az_AZ" , TEXTS_az_AZ, /* 11/ 18 Azerbaijani (Azerbaijan) */ "be_BY" , TEXTS_be_BY, /* 9/ 32 Belarusian (Belarus) */ - "bg" , TEXTS_bg, /* 2/ 8 Bulgarian */ + "bg" , TEXTS_bg, /* 2/ 9 Bulgarian */ + "bn_BD" , TEXTS_bn_BD, /* 2/ 12 Bengali (Bangladesh) */ "bn_IN" , TEXTS_bn_IN, /* 2/ 12 Bengali (India) */ - "ca" , TEXTS_ca, /* 11/ 96 Catalan */ + "ca" , TEXTS_ca, /* 11/ 99 Catalan */ "cs" , TEXTS_cs, /* 17/ 21 Czech */ - "da" , TEXTS_da, /* 19/ 54 Danish */ - "de" , TEXTS_de, /* 16/ 62 German */ - "el" , TEXTS_el, /* 1/ 4 Greek */ - "en" , TEXTS_en, /* 8/ 11 English */ - "eo" , TEXTS_eo, /* 26/118 Esperanto */ - "es" , TEXTS_es, /* 8/ 55 Spanish */ + "da" , TEXTS_da, /* 19/ 55 Danish */ + "de" , TEXTS_de, /* 16/ 66 German */ + "el" , TEXTS_el, /* 1/ 5 Greek */ + "en" , TEXTS_en, /* 8/ 10 English */ + "eo" , TEXTS_eo, /* 26/126 Esperanto */ + "es" , TEXTS_es, /* 8/ 56 Spanish */ "et_EE" , TEXTS_et_EE, /* 22/ 27 Estonian (Estonia) */ - "eu_ES" , TEXTS_eu_ES, /* 7/ 9 Basque (Spain) */ - "fa" , TEXTS_fa, /* 58/125 Persian */ - "fi" , TEXTS_fi, /* 10/ 54 Finnish */ - "fr" , TEXTS_fr, /* 13/ 62 French */ - "gl_ES" , TEXTS_gl_ES, /* 7/ 9 Gallegan (Spain) */ - "hi" , TEXTS_hi, /* 23/ 53 Hindi */ + "eu_ES" , TEXTS_eu_ES, /* 7/ 8 Basque (Spain) */ + "fa" , TEXTS_fa, /* 58/133 Persian */ + "fi" , TEXTS_fi, /* 10/ 55 Finnish */ + "fr" , TEXTS_fr, /* 13/ 66 French */ + "gl_ES" , TEXTS_gl_ES, /* 7/ 8 Gallegan (Spain) */ + "hi" , TEXTS_hi, /* 27/ 60 Hindi */ + "hi_ZZ" , TEXTS_hi_ZZ, /* 9/118 Hindi (ZZ) */ "hr" , TEXTS_hr, /* 9/ 20 Croatian */ "hu" , TEXTS_hu, /* 9/ 20 Hungarian */ - "hy_AM" , TEXTS_hy_AM, /* 9/126 Armenian (Armenia) */ + "hy_AM" , TEXTS_hy_AM, /* 9/134 Armenian (Armenia) */ "is" , TEXTS_is, /* 10/ 16 Icelandic */ - "it" , TEXTS_it, /* 11/ 62 Italian */ - "iw" , TEXTS_iw, /* 20/123 Hebrew */ - "ka_GE" , TEXTS_ka_GE, /* 3/ 10 Georgian (Georgia) */ - "kk" , TEXTS_kk, /* 15/121 Kazakh */ - "km_KH" , TEXTS_km_KH, /* 2/122 Khmer (Cambodia) */ + "it" , TEXTS_it, /* 11/ 66 Italian */ + "iw" , TEXTS_iw, /* 20/131 Hebrew */ + "ka_GE" , TEXTS_ka_GE, /* 3/ 11 Georgian (Georgia) */ + "kk" , TEXTS_kk, /* 15/129 Kazakh */ + "km_KH" , TEXTS_km_KH, /* 2/130 Khmer (Cambodia) */ "kn_IN" , TEXTS_kn_IN, /* 2/ 12 Kannada (India) */ - "ky" , TEXTS_ky, /* 10/ 89 Kirghiz */ + "ky" , TEXTS_ky, /* 10/ 92 Kirghiz */ "lo_LA" , TEXTS_lo_LA, /* 2/ 12 Lao (Laos) */ "lt" , TEXTS_lt, /* 18/ 22 Lithuanian */ "lv" , TEXTS_lv, /* 18/ 22 Latvian */ - "mk" , TEXTS_mk, /* 9/ 94 Macedonian */ + "mk" , TEXTS_mk, /* 9/ 97 Macedonian */ "ml_IN" , TEXTS_ml_IN, /* 2/ 12 Malayalam (India) */ "mn_MN" , TEXTS_mn_MN, /* 2/ 12 Mongolian (Mongolia) */ "mr_IN" , TEXTS_mr_IN, /* 23/ 53 Marathi (India) */ - "my_MM" , TEXTS_my_MM, /* 8/104 Burmese (Myanmar) */ - "nb" , TEXTS_nb, /* 11/ 54 Norwegian Bokmål */ - "ne_NP" , TEXTS_ne_NP, /* 23/ 53 Nepali (Nepal) */ + "my_MM" , TEXTS_my_MM, /* 8/ 98 Burmese (Myanmar) */ + "nb" , TEXTS_nb, /* 11/ 55 Norwegian Bokmål */ + "ne_NP" , TEXTS_ne_NP, /* 27/ 60 Nepali (Nepal) */ "nl" , TEXTS_nl, /* 9/ 13 Dutch */ "pl" , TEXTS_pl, /* 10/ 17 Polish */ - "pt" , TEXTS_pt, /* 6/ 7 Portuguese */ + "pt" , TEXTS_pt, /* 6/ 8 Portuguese */ "rm" , TEXTS_rm, /* 1/ 2 Raeto-Romance */ "ro" , TEXTS_ro, /* 6/ 16 Romanian */ "ru" , TEXTS_ru, /* 9/ 32 Russian */ "si_LK" , TEXTS_si_LK, /* 2/ 12 Sinhalese (Sri Lanka) */ "sk" , TEXTS_sk, /* 20/ 22 Slovak */ "sl" , TEXTS_sl, /* 8/ 20 Slovenian */ - "sr" , TEXTS_sr, /* 11/ 94 Serbian */ - "sv" , TEXTS_sv, /* 21/ 54 Swedish */ + "sr" , TEXTS_sr, /* 11/ 97 Serbian */ + "sr_ZZ" , TEXTS_sr_ZZ, /* 14/118 Serbian (ZZ) */ + "sv" , TEXTS_sv, /* 21/ 55 Swedish */ "sw" , TEXTS_sw, /* 9/ 18 Swahili */ "ta_IN" , TEXTS_ta_IN, /* 2/ 12 Tamil (India) */ "ta_LK" , TEXTS_ta_LK, /* 2/ 12 Tamil (Sri Lanka) */ - "ta_SG" , TEXTS_ta_SG, /* 1/ 4 Tamil (Singapore) */ + "ta_SG" , TEXTS_ta_SG, /* 1/ 5 Tamil (Singapore) */ "te_IN" , TEXTS_te_IN, /* 2/ 12 Telugu (India) */ "th" , TEXTS_th, /* 2/ 12 Thai */ - "tl" , TEXTS_tl, /* 7/ 9 Tagalog */ - "tr" , TEXTS_tr, /* 7/ 18 Turkish */ - "uk" , TEXTS_uk, /* 11/ 88 Ukrainian */ - "vi" , TEXTS_vi, /* 8/ 14 Vietnamese */ - "zu" , TEXTS_zu, /* 8/ 11 Zulu */ - "zz" , TEXTS_zz, /* 19/112 Alphabet */ + "tl" , TEXTS_tl, /* 7/ 8 Tagalog */ + "tr" , TEXTS_tr, /* 11/ 18 Turkish */ + "uk" , TEXTS_uk, /* 11/ 91 Ukrainian */ + "uz_UZ" , TEXTS_uz_UZ, /* 11/ 18 Uzbek (Uzbekistan) */ + "vi" , TEXTS_vi, /* 8/ 15 Vietnamese */ + "zu" , TEXTS_zu, /* 8/ 10 Zulu */ + "zz" , TEXTS_zz, /* 19/120 Alphabet */ }; static { diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java index 7743d4744..e8678637b 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java @@ -36,4 +36,12 @@ public final class KeysCache { mMap.put(key, key); return key; } + + public Key replace(final Key oldKey, final Key newKey) { + if (oldKey.equals(newKey)) { + return oldKey; + } + mMap.remove(oldKey); + return get(newKey); + } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java index 6400a2440..21eaed950 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java +++ b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java @@ -18,10 +18,12 @@ package com.android.inputmethod.keyboard.internal; import android.view.inputmethod.InputMethodSubtype; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import java.util.Collections; import java.util.List; +import java.util.Locale; /** * This class determines that the language name on the spacebar should be displayed in what format. @@ -30,19 +32,24 @@ public final class LanguageOnSpacebarHelper { public static final int FORMAT_TYPE_NONE = 0; public static final int FORMAT_TYPE_LANGUAGE_ONLY = 1; public static final int FORMAT_TYPE_FULL_LOCALE = 2; + public static final int FORMAT_TYPE_MULTIPLE = 3; private List<InputMethodSubtype> mEnabledSubtypes = Collections.emptyList(); private boolean mIsSystemLanguageSameAsInputLanguage; - public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) { - if (SubtypeLocaleUtils.isNoLanguage(subtype)) { + public int getLanguageOnSpacebarFormatType(final RichInputMethodSubtype subtype) { + if (subtype.isNoLanguage()) { return FORMAT_TYPE_FULL_LOCALE; } // Only this subtype is enabled and equals to the system locale. if (mEnabledSubtypes.size() < 2 && mIsSystemLanguageSameAsInputLanguage) { return FORMAT_TYPE_NONE; } - final String keyboardLanguage = SubtypeLocaleUtils.getSubtypeLocale(subtype).getLanguage(); + final Locale[] locales = subtype.getLocales(); + if (1 < locales.length) { + return FORMAT_TYPE_MULTIPLE; + } + final String keyboardLanguage = locales[0].getLanguage(); final String keyboardLayout = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype); int sameLanguageAndLayoutCount = 0; for (final InputMethodSubtype ims : mEnabledSubtypes) { diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java index 625a0c283..0cd031e5f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java +++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java @@ -17,7 +17,9 @@ package com.android.inputmethod.keyboard.internal; import android.text.TextUtils; +import android.util.SparseIntArray; +import com.android.inputmethod.compat.CharacterCompat; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.define.DebugFlags; @@ -26,6 +28,7 @@ import com.android.inputmethod.latin.utils.StringUtils; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.Locale; /** @@ -110,6 +113,51 @@ public final class MoreKeySpec { } } + public static class LettersOnBaseLayout { + private final SparseIntArray mCodes = new SparseIntArray(); + private final HashSet<String> mTexts = new HashSet<>(); + + public void addLetter(final Key key) { + final int code = key.getCode(); + if (CharacterCompat.isAlphabetic(code)) { + mCodes.put(code, 0); + } else if (code == Constants.CODE_OUTPUT_TEXT) { + mTexts.add(key.getOutputText()); + } + } + + public boolean contains(final MoreKeySpec moreKey) { + final int code = moreKey.mCode; + if (CharacterCompat.isAlphabetic(code) && mCodes.indexOfKey(code) >= 0) { + return true; + } else if (code == Constants.CODE_OUTPUT_TEXT && mTexts.contains(moreKey.mOutputText)) { + return true; + } + return false; + } + } + + public static MoreKeySpec[] removeRedundantMoreKeys(final MoreKeySpec[] moreKeys, + final LettersOnBaseLayout lettersOnBaseLayout) { + if (moreKeys == null) { + return null; + } + final ArrayList<MoreKeySpec> filteredMoreKeys = new ArrayList<>(); + for (final MoreKeySpec moreKey : moreKeys) { + if (!lettersOnBaseLayout.contains(moreKey)) { + filteredMoreKeys.add(moreKey); + } + } + final int size = filteredMoreKeys.size(); + if (size == moreKeys.length) { + return moreKeys; + } + if (size == 0) { + return null; + } + return filteredMoreKeys.toArray(new MoreKeySpec[size]); + } + private static final boolean DEBUG = DebugFlags.DEBUG_ENABLED; // Constants for parsing. private static final char COMMA = Constants.CODE_COMMA; diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java index 8e89e61ea..556d74f4b 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java +++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java @@ -95,7 +95,7 @@ public final class PointerTrackerQueue { public void releaseAllPointersOlderThan(final Element pointer, final long eventTime) { synchronized (mExpandableArrayOfActivePointers) { if (DEBUG) { - Log.d(TAG, "releaseAllPoniterOlderThan: " + pointer + " " + this); + Log.d(TAG, "releaseAllPointerOlderThan: " + pointer + " " + this); } final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; final int arraySize = mArraySize; @@ -144,9 +144,9 @@ public final class PointerTrackerQueue { synchronized (mExpandableArrayOfActivePointers) { if (DEBUG) { if (pointer == null) { - Log.d(TAG, "releaseAllPoniters: " + this); + Log.d(TAG, "releaseAllPointers: " + this); } else { - Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this); + Log.d(TAG, "releaseAllPointerExcept: " + pointer + " " + this); } } final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; diff --git a/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java b/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java index ec7b9b024..80b299bf5 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java +++ b/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java @@ -27,6 +27,8 @@ import com.android.inputmethod.keyboard.internal.TimerHandler.Callbacks; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; +import javax.annotation.Nonnull; + // TODO: Separate this class into KeyTimerHandler and BatchInputTimerHandler or so. public final class TimerHandler extends LeakGuardHandlerWrapper<Callbacks> implements TimerProxy { public interface Callbacks { @@ -45,7 +47,7 @@ public final class TimerHandler extends LeakGuardHandlerWrapper<Callbacks> imple private final int mIgnoreAltCodeKeyTimeout; private final int mGestureRecognitionUpdateTime; - public TimerHandler(final Callbacks ownerInstance, final int ignoreAltCodeKeyTimeout, + public TimerHandler(@Nonnull final Callbacks ownerInstance, final int ignoreAltCodeKeyTimeout, final int gestureRecognitionUpdateTime) { super(ownerInstance); mIgnoreAltCodeKeyTimeout = ignoreAltCodeKeyTimeout; diff --git a/java/src/com/android/inputmethod/latin/BackupAgent.java b/java/src/com/android/inputmethod/latin/BackupAgent.java index 1f044618a..b2d92b30c 100644 --- a/java/src/com/android/inputmethod/latin/BackupAgent.java +++ b/java/src/com/android/inputmethod/latin/BackupAgent.java @@ -17,15 +17,41 @@ package com.android.inputmethod.latin; import android.app.backup.BackupAgentHelper; +import android.app.backup.BackupDataInput; import android.app.backup.SharedPreferencesBackupHelper; +import android.content.SharedPreferences; +import android.os.ParcelFileDescriptor; + +import com.android.inputmethod.latin.settings.LocalSettingsConstants; + +import java.io.IOException; /** - * Backs up the Latin IME shared preferences. + * Backup/restore agent for LatinIME. + * Currently it backs up the default shared preferences. */ public final class BackupAgent extends BackupAgentHelper { + private static final String PREF_SUFFIX = "_preferences"; + @Override public void onCreate() { addHelper("shared_pref", new SharedPreferencesBackupHelper(this, - getPackageName() + "_preferences")); + getPackageName() + PREF_SUFFIX)); + } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) + throws IOException { + // Let the restore operation go through + super.onRestore(data, appVersionCode, newState); + + // Remove the preferences that we don't want restored. + final SharedPreferences.Editor prefEditor = getSharedPreferences( + getPackageName() + PREF_SUFFIX, MODE_PRIVATE).edit(); + for (final String key : LocalSettingsConstants.PREFS_TO_SKIP_RESTORING) { + prefEditor.remove(key); + } + // Flush the changes to disk. + prefEditor.commit(); } } diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index 693e1cdcc..2fece7c85 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -42,6 +42,8 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; +import javax.annotation.Nonnull; + /** * Implements a static, compacted, binary dictionary of standard words. */ @@ -68,7 +70,7 @@ public final class BinaryDictionary extends Dictionary { private static final int FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT = 5; private static final int FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX = 0; private static final int FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX = 1; - private static final int FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX = 2; + private static final int FORMAT_WORD_PROPERTY_HAS_NGRAMS_INDEX = 2; private static final int FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX = 3; private static final int FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX = 4; @@ -83,7 +85,6 @@ public final class BinaryDictionary extends Dictionary { public static final String DIR_NAME_SUFFIX_FOR_RECORD_MIGRATION = ".migrating"; private long mNativeDict; - private final Locale mLocale; private final long mDictSize; private final String mDictFilePath; private final boolean mUseFullEditDistance; @@ -117,8 +118,7 @@ public final class BinaryDictionary extends Dictionary { public BinaryDictionary(final String filename, final long offset, final long length, final boolean useFullEditDistance, final Locale locale, final String dictType, final boolean isUpdatable) { - super(dictType); - mLocale = locale; + super(dictType, locale); mDictSize = length; mDictFilePath = filename; mIsUpdatable = isUpdatable; @@ -138,8 +138,7 @@ public final class BinaryDictionary extends Dictionary { public BinaryDictionary(final String filename, final boolean useFullEditDistance, final Locale locale, final String dictType, final long formatVersion, final Map<String, String> attributeMap) { - super(dictType); - mLocale = locale; + super(dictType, locale); mDictSize = 0; mDictFilePath = filename; // On memory dictionary is always updatable. @@ -180,18 +179,20 @@ public final class BinaryDictionary extends Dictionary { boolean[] isBeginningOfSentenceArray, int[] word); private static native void getWordPropertyNative(long dict, int[] word, boolean isBeginningOfSentence, int[] outCodePoints, boolean[] outFlags, - int[] outProbabilityInfo, ArrayList<int[]> outBigramTargets, - ArrayList<int[]> outBigramProbabilityInfo, ArrayList<int[]> outShortcutTargets, - ArrayList<Integer> outShortcutProbabilities); + int[] outProbabilityInfo, ArrayList<int[][]> outNgramPrevWordsArray, + ArrayList<boolean[]> outNgramPrevWordIsBeginningOfSentenceArray, + ArrayList<int[]> outNgramTargets, ArrayList<int[]> outNgramProbabilityInfo, + ArrayList<int[]> outShortcutTargets, ArrayList<Integer> outShortcutProbabilities); private static native int getNextWordNative(long dict, int token, int[] outCodePoints, boolean[] outIsBeginningOfSentence); private static native void getSuggestionsNative(long dict, long proximityInfo, long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times, int[] pointerIds, int[] inputCodePoints, int inputSize, int[] suggestOptions, int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray, - int[] outputSuggestionCount, int[] outputCodePoints, int[] outputScores, - int[] outputIndices, int[] outputTypes, int[] outputAutoCommitFirstWordConfidence, - float[] inOutLanguageWeight); + int prevWordCount, int[] outputSuggestionCount, int[] outputCodePoints, + int[] outputScores, int[] outputIndices, int[] outputTypes, + int[] outputAutoCommitFirstWordConfidence, + float[] inOutWeightOfLangModelVsSpatialModel); private static native boolean addUnigramEntryNative(long dict, int[] word, int probability, int[] shortcutTarget, int shortcutProbability, boolean isBeginningOfSentence, boolean isNotAWord, boolean isBlacklisted, int timestamp); @@ -201,6 +202,9 @@ public final class BinaryDictionary extends Dictionary { int[] word, int probability, int timestamp); private static native boolean removeNgramEntryNative(long dict, int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray, int[] word); + private static native boolean updateEntriesForWordWithNgramContextNative(long dict, + int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray, + int[] word, boolean isValidWord, int count, int timestamp); private static native int addMultipleDictionaryEntriesNative(long dict, LanguageModelParam[] languageModelParams, int startIndex); private static native String getPropertyNative(long dict, String query); @@ -257,15 +261,16 @@ public final class BinaryDictionary extends Dictionary { @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final NgramContext ngramContext, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, - final int sessionId, final float[] inOutLanguageWeight) { + final int sessionId, final float weightForLocale, + final float[] inOutWeightOfLangModelVsSpatialModel) { if (!isValidDictionary()) { return null; } final DicTraverseSession session = getTraverseSession(sessionId); Arrays.fill(session.mInputCodePoints, Constants.NOT_A_CODE); - prevWordsInfo.outputToArray(session.mPrevWordCodePointArrays, + ngramContext.outputToArray(session.mPrevWordCodePointArrays, session.mIsBeginningOfSentenceArray); final InputPointers inputPointers = composer.getInputPointers(); final boolean isGesture = composer.isBatchMode(); @@ -287,10 +292,13 @@ public final class BinaryDictionary extends Dictionary { settingsValuesForSuggestion.mSpaceAwareGestureEnabled); session.mNativeSuggestOptions.setAdditionalFeaturesOptions( settingsValuesForSuggestion.mAdditionalFeaturesSettingValues); - if (inOutLanguageWeight != null) { - session.mInputOutputLanguageWeight[0] = inOutLanguageWeight[0]; + session.mNativeSuggestOptions.setWeightForLocale(weightForLocale); + if (inOutWeightOfLangModelVsSpatialModel != null) { + session.mInputOutputWeightOfLangModelVsSpatialModel[0] = + inOutWeightOfLangModelVsSpatialModel[0]; } else { - session.mInputOutputLanguageWeight[0] = Dictionary.NOT_A_LANGUAGE_WEIGHT; + session.mInputOutputWeightOfLangModelVsSpatialModel[0] = + Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL; } // TOOD: Pass multiple previous words information for n-gram. getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(), @@ -298,12 +306,14 @@ public final class BinaryDictionary extends Dictionary { inputPointers.getYCoordinates(), inputPointers.getTimes(), inputPointers.getPointerIds(), session.mInputCodePoints, inputSize, session.mNativeSuggestOptions.getOptions(), session.mPrevWordCodePointArrays, - session.mIsBeginningOfSentenceArray, session.mOutputSuggestionCount, - session.mOutputCodePoints, session.mOutputScores, session.mSpaceIndices, - session.mOutputTypes, session.mOutputAutoCommitFirstWordConfidence, - session.mInputOutputLanguageWeight); - if (inOutLanguageWeight != null) { - inOutLanguageWeight[0] = session.mInputOutputLanguageWeight[0]; + session.mIsBeginningOfSentenceArray, ngramContext.getPrevWordCount(), + session.mOutputSuggestionCount, session.mOutputCodePoints, session.mOutputScores, + session.mSpaceIndices, session.mOutputTypes, + session.mOutputAutoCommitFirstWordConfidence, + session.mInputOutputWeightOfLangModelVsSpatialModel); + if (inOutWeightOfLangModelVsSpatialModel != null) { + inOutWeightOfLangModelVsSpatialModel[0] = + session.mInputOutputWeightOfLangModelVsSpatialModel[0]; } final int count = session.mOutputSuggestionCount[0]; final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>(); @@ -317,7 +327,8 @@ public final class BinaryDictionary extends Dictionary { if (len > 0) { suggestions.add(new SuggestedWordInfo( new String(session.mOutputCodePoints, start, len), - session.mOutputScores[j], session.mOutputTypes[j], this /* sourceDict */, + (int)(session.mOutputScores[j] * weightForLocale), session.mOutputTypes[j], + this /* sourceDict */, session.mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */, session.mOutputAutoCommitFirstWordConfidence[0])); } @@ -353,18 +364,17 @@ public final class BinaryDictionary extends Dictionary { } @UsedForTesting - public boolean isValidNgram(final PrevWordsInfo prevWordsInfo, final String word) { - return getNgramProbability(prevWordsInfo, word) != NOT_A_PROBABILITY; + public boolean isValidNgram(final NgramContext ngramContext, final String word) { + return getNgramProbability(ngramContext, word) != NOT_A_PROBABILITY; } - public int getNgramProbability(final PrevWordsInfo prevWordsInfo, final String word) { - if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) { + public int getNgramProbability(final NgramContext ngramContext, final String word) { + if (!ngramContext.isValid() || TextUtils.isEmpty(word)) { return NOT_A_PROBABILITY; } - final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][]; - final boolean[] isBeginningOfSentenceArray = - new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; - prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray); + final int[][] prevWordCodePointArrays = new int[ngramContext.getPrevWordCount()][]; + final boolean[] isBeginningOfSentenceArray = new boolean[ngramContext.getPrevWordCount()]; + ngramContext.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray); final int[] wordCodePoints = StringUtils.toCodePointArray(word); return getNgramProbabilityNative(mNativeDict, prevWordCodePointArrays, isBeginningOfSentenceArray, wordCodePoints); @@ -379,20 +389,25 @@ public final class BinaryDictionary extends Dictionary { final boolean[] outFlags = new boolean[FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT]; final int[] outProbabilityInfo = new int[FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT]; - final ArrayList<int[]> outBigramTargets = new ArrayList<>(); - final ArrayList<int[]> outBigramProbabilityInfo = new ArrayList<>(); + final ArrayList<int[][]> outNgramPrevWordsArray = new ArrayList<>(); + final ArrayList<boolean[]> outNgramPrevWordIsBeginningOfSentenceArray = + new ArrayList<>(); + final ArrayList<int[]> outNgramTargets = new ArrayList<>(); + final ArrayList<int[]> outNgramProbabilityInfo = new ArrayList<>(); final ArrayList<int[]> outShortcutTargets = new ArrayList<>(); final ArrayList<Integer> outShortcutProbabilities = new ArrayList<>(); getWordPropertyNative(mNativeDict, codePoints, isBeginningOfSentence, outCodePoints, - outFlags, outProbabilityInfo, outBigramTargets, outBigramProbabilityInfo, - outShortcutTargets, outShortcutProbabilities); + outFlags, outProbabilityInfo, outNgramPrevWordsArray, + outNgramPrevWordIsBeginningOfSentenceArray, outNgramTargets, + outNgramProbabilityInfo, outShortcutTargets, outShortcutProbabilities); return new WordProperty(codePoints, outFlags[FORMAT_WORD_PROPERTY_IS_NOT_A_WORD_INDEX], outFlags[FORMAT_WORD_PROPERTY_IS_BLACKLISTED_INDEX], - outFlags[FORMAT_WORD_PROPERTY_HAS_BIGRAMS_INDEX], + outFlags[FORMAT_WORD_PROPERTY_HAS_NGRAMS_INDEX], outFlags[FORMAT_WORD_PROPERTY_HAS_SHORTCUTS_INDEX], outFlags[FORMAT_WORD_PROPERTY_IS_BEGINNING_OF_SENTENCE_INDEX], outProbabilityInfo, - outBigramTargets, outBigramProbabilityInfo, outShortcutTargets, + outNgramPrevWordsArray, outNgramPrevWordIsBeginningOfSentenceArray, + outNgramTargets, outNgramProbabilityInfo, outShortcutTargets, outShortcutProbabilities); } @@ -453,15 +468,14 @@ public final class BinaryDictionary extends Dictionary { } // Add an n-gram entry to the binary dictionary with timestamp in native code. - public boolean addNgramEntry(final PrevWordsInfo prevWordsInfo, final String word, + public boolean addNgramEntry(final NgramContext ngramContext, final String word, final int probability, final int timestamp) { - if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) { + if (!ngramContext.isValid() || TextUtils.isEmpty(word)) { return false; } - final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][]; - final boolean[] isBeginningOfSentenceArray = - new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; - prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray); + final int[][] prevWordCodePointArrays = new int[ngramContext.getPrevWordCount()][]; + final boolean[] isBeginningOfSentenceArray = new boolean[ngramContext.getPrevWordCount()]; + ngramContext.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray); final int[] wordCodePoints = StringUtils.toCodePointArray(word); if (!addNgramEntryNative(mNativeDict, prevWordCodePointArrays, isBeginningOfSentenceArray, wordCodePoints, probability, timestamp)) { @@ -472,14 +486,13 @@ public final class BinaryDictionary extends Dictionary { } // Remove an n-gram entry from the binary dictionary in native code. - public boolean removeNgramEntry(final PrevWordsInfo prevWordsInfo, final String word) { - if (!prevWordsInfo.isValid() || TextUtils.isEmpty(word)) { + public boolean removeNgramEntry(final NgramContext ngramContext, final String word) { + if (!ngramContext.isValid() || TextUtils.isEmpty(word)) { return false; } - final int[][] prevWordCodePointArrays = new int[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM][]; - final boolean[] isBeginningOfSentenceArray = - new boolean[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; - prevWordsInfo.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray); + final int[][] prevWordCodePointArrays = new int[ngramContext.getPrevWordCount()][]; + final boolean[] isBeginningOfSentenceArray = new boolean[ngramContext.getPrevWordCount()]; + ngramContext.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray); final int[] wordCodePoints = StringUtils.toCodePointArray(word); if (!removeNgramEntryNative(mNativeDict, prevWordCodePointArrays, isBeginningOfSentenceArray, wordCodePoints)) { @@ -489,6 +502,25 @@ public final class BinaryDictionary extends Dictionary { return true; } + // Update entries for the word occurrence with the ngramContext. + public boolean updateEntriesForWordWithNgramContext(@Nonnull final NgramContext ngramContext, + final String word, final boolean isValidWord, final int count, final int timestamp) { + if (TextUtils.isEmpty(word)) { + return false; + } + final int[][] prevWordCodePointArrays = new int[ngramContext.getPrevWordCount()][]; + final boolean[] isBeginningOfSentenceArray = new boolean[ngramContext.getPrevWordCount()]; + ngramContext.outputToArray(prevWordCodePointArrays, isBeginningOfSentenceArray); + final int[] wordCodePoints = StringUtils.toCodePointArray(word); + if (!updateEntriesForWordWithNgramContextNative(mNativeDict, prevWordCodePointArrays, + isBeginningOfSentenceArray, wordCodePoints, isValidWord, count, timestamp)) { + return false; + } + mHasUpdated = true; + return true; + } + + @UsedForTesting public void addMultipleDictionaryEntries(final LanguageModelParam[] languageModelParams) { if (!isValidDictionary()) return; int processedParamCount = 0; @@ -596,7 +628,7 @@ public final class BinaryDictionary extends Dictionary { } @UsedForTesting - public String getPropertyForTest(final String query) { + public String getPropertyForGettingStats(final String query) { if (!isValidDictionary()) return ""; return getPropertyNative(mNativeDict, query); } diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 43af66eb7..fc7f95c7b 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -57,6 +57,13 @@ public final class Constants { @SuppressWarnings("dep-ann") public static final String FORCE_ASCII = "forceAscii"; + /** + * The private IME option used to suppress the floating gesture preview for a given text + * field. This overrides the corresponding keyboard settings preference. + * {@link com.android.inputmethod.latin.settings.SettingsValues#mGestureFloatingPreviewTextEnabled} + */ + public static final String NO_FLOATING_GESTURE_PREVIEW = "noGestureFloatingPreview"; + private ImeOption() { // This utility class is not publicly instantiable. } @@ -217,10 +224,12 @@ public final class Constants { public static final int CODE_CLOSING_ANGLE_BRACKET = '>'; public static final int CODE_INVERTED_QUESTION_MARK = 0xBF; // ¿ public static final int CODE_INVERTED_EXCLAMATION_MARK = 0xA1; // ¡ + public static final int CODE_GRAVE_ACCENT = '`'; + public static final int CODE_CIRCUMFLEX_ACCENT = '^'; + public static final int CODE_TILDE = '~'; 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/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java index 162a209e3..78c6cbd24 100644 --- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java @@ -218,7 +218,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { */ private void addNameLocked(final String name) { int len = StringUtils.codePointCount(name); - PrevWordsInfo prevWordsInfo = PrevWordsInfo.EMPTY_PREV_WORDS_INFO; + NgramContext ngramContext = NgramContext.EMPTY_PREV_WORDS_INFO; // TODO: Better tokenization for non-Latin writing systems for (int i = 0; i < len; i++) { if (Character.isLetter(name.codePointAt(i))) { @@ -233,19 +233,19 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { final int wordLen = StringUtils.codePointCount(word); if (wordLen <= MAX_WORD_LENGTH && wordLen > 1) { if (DEBUG) { - Log.d(TAG, "addName " + name + ", " + word + ", " + prevWordsInfo); + Log.d(TAG, "addName " + name + ", " + word + ", " + ngramContext); } runGCIfRequiredLocked(true /* mindsBlockByGC */); addUnigramLocked(word, FREQUENCY_FOR_CONTACTS, null /* shortcut */, 0 /* shortcutFreq */, false /* isNotAWord */, false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP); - if (!prevWordsInfo.isValid() && mUseFirstLastBigrams) { + if (!ngramContext.isValid() && mUseFirstLastBigrams) { runGCIfRequiredLocked(true /* mindsBlockByGC */); - addNgramEntryLocked(prevWordsInfo, word, FREQUENCY_FOR_CONTACTS_BIGRAM, + addNgramEntryLocked(ngramContext, word, FREQUENCY_FOR_CONTACTS_BIGRAM, BinaryDictionary.NOT_A_VALID_TIMESTAMP); } - prevWordsInfo = prevWordsInfo.getNextPrevWordsInfo( - new PrevWordsInfo.WordInfo(word)); + ngramContext = ngramContext.getNextNgramContext( + new NgramContext.WordInfo(word)); } } } diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java index b341f623e..2751c1250 100644 --- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java +++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java @@ -40,7 +40,7 @@ public final class DicTraverseSession { public final int[] mOutputTypes = new int[MAX_RESULTS]; // Only one result is ever used public final int[] mOutputAutoCommitFirstWordConfidence = new int[1]; - public final float[] mInputOutputLanguageWeight = new float[1]; + public final float[] mInputOutputWeightOfLangModelVsSpatialModel = new float[1]; public final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions(); diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index 560ced9c4..e66847b56 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -22,6 +22,9 @@ import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import java.util.ArrayList; +import java.util.Locale; +import java.util.Arrays; +import java.util.HashSet; /** * Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key @@ -29,24 +32,24 @@ import java.util.ArrayList; */ public abstract class Dictionary { public static final int NOT_A_PROBABILITY = -1; - public static final float NOT_A_LANGUAGE_WEIGHT = -1.0f; + public static final float NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL = -1.0f; // The following types do not actually come from real dictionary instances, so we create // corresponding instances. public static final String TYPE_USER_TYPED = "user_typed"; - public static final Dictionary DICTIONARY_USER_TYPED = new PhonyDictionary(TYPE_USER_TYPED); + public static final PhonyDictionary DICTIONARY_USER_TYPED = new PhonyDictionary(TYPE_USER_TYPED); public static final String TYPE_APPLICATION_DEFINED = "application_defined"; - public static final Dictionary DICTIONARY_APPLICATION_DEFINED = + public static final PhonyDictionary DICTIONARY_APPLICATION_DEFINED = new PhonyDictionary(TYPE_APPLICATION_DEFINED); public static final String TYPE_HARDCODED = "hardcoded"; // punctuation signs and such - public static final Dictionary DICTIONARY_HARDCODED = + public static final PhonyDictionary DICTIONARY_HARDCODED = new PhonyDictionary(TYPE_HARDCODED); // Spawned by resuming suggestions. Comes from a span that was in the TextView. public static final String TYPE_RESUMED = "resumed"; - public static final Dictionary DICTIONARY_RESUMED = + public static final PhonyDictionary DICTIONARY_RESUMED = new PhonyDictionary(TYPE_RESUMED); // The following types of dictionary have actual functional instances. We don't need final @@ -62,28 +65,41 @@ public abstract class Dictionary { // Contextual dictionary. public static final String TYPE_CONTEXTUAL = "contextual"; public final String mDictType; + // The locale for this dictionary. May be null if unknown (phony dictionary for example). + public final Locale mLocale; - public Dictionary(final String dictType) { + /** + * Set out of the dictionary types listed above that are based on data specific to the user, + * e.g., the user's contacts. + */ + private static final HashSet<String> sUserSpecificDictionaryTypes = + new HashSet(Arrays.asList(new String[] { TYPE_USER_TYPED, TYPE_USER, TYPE_CONTACTS, + TYPE_USER_HISTORY, TYPE_PERSONALIZATION, TYPE_CONTEXTUAL })); + + public Dictionary(final String dictType, final Locale locale) { mDictType = dictType; + mLocale = locale; } /** - * Searches for suggestions for a given context. For the moment the context is only the - * previous word. + * Searches for suggestions for a given context. * @param composer the key sequence to match with coordinate info, as a WordComposer - * @param prevWordsInfo the information of previous words. + * @param ngramContext the context for n-gram. * @param proximityInfo the object for key proximity. May be ignored by some implementations. * @param settingsValuesForSuggestion the settings values used for the suggestion. * @param sessionId the session id. - * @param inOutLanguageWeight the language weight used for generating suggestions. - * inOutLanguageWeight is a float array that has only one element. This can be updated when the - * different language weight is used. + * @param weightForLocale the weight given to this locale, to multiply the output scores for + * multilingual input. + * @param inOutWeightOfLangModelVsSpatialModel the weight of the language model as a ratio of + * the spatial model, used for generating suggestions. inOutWeightOfLangModelVsSpatialModel is + * a float array that has only one element. This can be updated when a different value is used. * @return the list of suggestions (possibly null if none) */ abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final NgramContext ngramContext, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, - final int sessionId, final float[] inOutLanguageWeight); + final int sessionId, final float weightForLocale, + final float[] inOutWeightOfLangModelVsSpatialModel); /** * Checks if the given word has to be treated as a valid word. Please note that some @@ -156,20 +172,30 @@ public abstract class Dictionary { } /** + * Whether this dictionary is based on data specific to the user, e.g., the user's contacts. + * @return Whether this dictionary is specific to the user. + */ + public boolean isUserSpecific() { + return sUserSpecificDictionaryTypes.contains(mDictType); + } + + /** * Not a true dictionary. A placeholder used to indicate suggestions that don't come from any * real dictionary. */ - private static class PhonyDictionary extends Dictionary { - // This class is not publicly instantiable. - private PhonyDictionary(final String type) { - super(type); + @UsedForTesting + static class PhonyDictionary extends Dictionary { + @UsedForTesting + PhonyDictionary(final String type) { + super(type, null); } @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final NgramContext ngramContext, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, - final int sessionId, final float[] inOutLanguageWeight) { + final int sessionId, final float weightForLocale, + final float[] inOutWeightOfLangModelVsSpatialModel) { return null; } diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java index 2b4c54d48..a6d7205e2 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java +++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java @@ -25,6 +25,7 @@ import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Locale; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -34,13 +35,14 @@ public final class DictionaryCollection extends Dictionary { private final String TAG = DictionaryCollection.class.getSimpleName(); protected final CopyOnWriteArrayList<Dictionary> mDictionaries; - public DictionaryCollection(final String dictType) { - super(dictType); + public DictionaryCollection(final String dictType, final Locale locale) { + super(dictType, locale); mDictionaries = new CopyOnWriteArrayList<>(); } - public DictionaryCollection(final String dictType, final Dictionary... dictionaries) { - super(dictType); + public DictionaryCollection(final String dictType, final Locale locale, + final Dictionary... dictionaries) { + super(dictType, locale); if (null == dictionaries) { mDictionaries = new CopyOnWriteArrayList<>(); } else { @@ -49,30 +51,32 @@ public final class DictionaryCollection extends Dictionary { } } - public DictionaryCollection(final String dictType, final Collection<Dictionary> dictionaries) { - super(dictType); + public DictionaryCollection(final String dictType, final Locale locale, + final Collection<Dictionary> dictionaries) { + super(dictType, locale); mDictionaries = new CopyOnWriteArrayList<>(dictionaries); mDictionaries.removeAll(Collections.singleton(null)); } @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final NgramContext ngramContext, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, - final int sessionId, final float[] inOutLanguageWeight) { + final int sessionId, final float weightForLocale, + final float[] inOutWeightOfLangModelVsSpatialModel) { final CopyOnWriteArrayList<Dictionary> dictionaries = mDictionaries; if (dictionaries.isEmpty()) return null; // To avoid creating unnecessary objects, we get the list out of the first // dictionary and add the rest to it if not null, hence the get(0) ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composer, - prevWordsInfo, proximityInfo, settingsValuesForSuggestion, sessionId, - inOutLanguageWeight); + ngramContext, proximityInfo, settingsValuesForSuggestion, sessionId, + weightForLocale, inOutWeightOfLangModelVsSpatialModel); if (null == suggestions) suggestions = new ArrayList<>(); final int length = dictionaries.size(); for (int i = 1; i < length; ++ i) { final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer, - prevWordsInfo, proximityInfo, settingsValuesForSuggestion, sessionId, - inOutLanguageWeight); + ngramContext, proximityInfo, settingsValuesForSuggestion, sessionId, + weightForLocale, inOutWeightOfLangModelVsSpatialModel); if (null != sugg) suggestions.addAll(sugg); } return suggestions; diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java index fd1f51dd6..08035dfd6 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -19,11 +19,13 @@ package com.android.inputmethod.latin; import android.content.Context; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.PrevWordsInfo.WordInfo; +import com.android.inputmethod.latin.ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback; +import com.android.inputmethod.latin.NgramContext.WordInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.personalization.ContextualDictionary; import com.android.inputmethod.latin.personalization.PersonalizationDataChunk; @@ -32,9 +34,9 @@ import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import com.android.inputmethod.latin.utils.DistracterFilter; +import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions; import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary; import com.android.inputmethod.latin.utils.ExecutorUtils; -import com.android.inputmethod.latin.utils.LanguageModelParam; import com.android.inputmethod.latin.utils.SuggestionResults; import java.io.File; @@ -59,12 +61,14 @@ public class DictionaryFacilitator { // dictionary. private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140; - private Dictionaries mDictionaries = new Dictionaries(); + private DictionaryGroup[] mDictionaryGroups = new DictionaryGroup[] { new DictionaryGroup() }; + private DictionaryGroup mMostProbableDictionaryGroup = mDictionaryGroups[0]; private boolean mIsUserDictEnabled = false; - private volatile CountDownLatch mLatchForWaitingLoadingMainDictionary = new CountDownLatch(0); - // To synchronize assigning mDictionaries to ensure closing dictionaries. + private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0); + // To synchronize assigning mDictionaryGroup to ensure closing dictionaries. private final Object mLock = new Object(); private final DistracterFilter mDistracterFilter; + private final PersonalizationHelperForDictionaryFacilitator mPersonalizationHelper; private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS = new String[] { @@ -96,22 +100,54 @@ public class DictionaryFacilitator { DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS.length); /** - * Class contains dictionaries for a locale. + * Returns whether this facilitator is exactly for this list of locales. + * @param locales the list of locales to test against + * @return true if this facilitator handles exactly this list of locales, false otherwise */ - private static class Dictionaries { + public boolean isForLocales(final Locale[] locales) { + if (locales.length != mDictionaryGroups.length) { + return false; + } + for (final Locale locale : locales) { + boolean found = false; + for (final DictionaryGroup group : mDictionaryGroups) { + if (locale.equals(group.mLocale)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + /** + * A group of dictionaries that work together for a single language. + */ + private static class DictionaryGroup { + // TODO: Run evaluation to determine a reasonable value for these constants. The current + // values are ad-hoc and chosen without any particular care or methodology. + public static final float WEIGHT_FOR_MOST_PROBABLE_LANGUAGE = 1.0f; + public static final float WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.95f; + public static final float WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.6f; + public final Locale mLocale; private Dictionary mMainDict; + public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; + public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; 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()); @@ -172,18 +208,72 @@ public class DictionaryFacilitator { public DictionaryFacilitator() { mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER; + mPersonalizationHelper = null; } - public DictionaryFacilitator(final DistracterFilter distracterFilter) { - mDistracterFilter = distracterFilter; + public DictionaryFacilitator(final Context context) { + mDistracterFilter = new DistracterFilterCheckingExactMatchesAndSuggestions(context); + mPersonalizationHelper = + new PersonalizationHelperForDictionaryFacilitator(context, mDistracterFilter); } public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { mDistracterFilter.updateEnabledSubtypes(enabledSubtypes); + mPersonalizationHelper.updateEnabledSubtypes(enabledSubtypes); + } + + // TODO: remove this, it's confusing with seamless multiple language switching + public void setIsMonolingualUser(final boolean isMonolingualUser) { + mPersonalizationHelper.setIsMonolingualUser(isMonolingualUser); } - public Locale getLocale() { - return mDictionaries.mLocale; + public boolean isActive() { + return null != mDictionaryGroups[0].mLocale; + } + + /** + * Returns the most probable locale among all currently active locales. BE CAREFUL using this. + * + * DO NOT USE THIS just because it's convenient. Use it when it's correct, for example when + * choosing what dictionary to put a word in, or when changing the capitalization of a typed + * string. + * @return the most probable locale + */ + public Locale getMostProbableLocale() { + return getDictionaryGroupForMostProbableLanguage().mLocale; + } + + public Locale[] getLocales() { + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + final Locale[] locales = new Locale[dictionaryGroups.length]; + for (int i = 0; i < dictionaryGroups.length; ++i) { + locales[i] = dictionaryGroups[i].mLocale; + } + return locales; + } + + private DictionaryGroup getDictionaryGroupForMostProbableLanguage() { + return mMostProbableDictionaryGroup; + } + + public void switchMostProbableLanguage(final Locale locale) { + if (null == locale) { + // In many cases, there is no locale to a committed word. For example, a typed word + // that does not auto-correct has no locale. In this case we simply do not change + // the most probable language. + return; + } + final DictionaryGroup newMostProbableDictionaryGroup = + findDictionaryGroupWithLocale(mDictionaryGroups, locale); + mMostProbableDictionaryGroup.mWeightForTypingInLocale = + DictionaryGroup.WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE; + mMostProbableDictionaryGroup.mWeightForGesturingInLocale = + DictionaryGroup.WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE; + newMostProbableDictionaryGroup.mWeightForTypingInLocale = + DictionaryGroup.WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; + newMostProbableDictionaryGroup.mWeightForGesturingInLocale = + DictionaryGroup.WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; + mMostProbableDictionaryGroup = newMostProbableDictionaryGroup; } private static ExpandableBinaryDictionary getSubDict(final String dictType, @@ -207,105 +297,153 @@ public class DictionaryFacilitator { } } - public void resetDictionaries(final Context context, final Locale newLocale, + public void resetDictionaries(final Context context, final Locale[] newLocales, final boolean useContactsDict, final boolean usePersonalizedDicts, final boolean forceReloadMainDictionary, final DictionaryInitializationListener listener) { - resetDictionariesWithDictNamePrefix(context, newLocale, useContactsDict, + resetDictionariesWithDictNamePrefix(context, newLocales, useContactsDict, usePersonalizedDicts, forceReloadMainDictionary, listener, "" /* dictNamePrefix */); } - public void resetDictionariesWithDictNamePrefix(final Context context, final Locale newLocale, + private DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup[] dictionaryGroups, + final Locale locale) { + for (int i = 0; i < dictionaryGroups.length; ++i) { + if (locale.equals(dictionaryGroups[i].mLocale)) { + return dictionaryGroups[i]; + } + } + return null; + } + + public void resetDictionariesWithDictNamePrefix(final Context context, + final Locale[] newLocales, final boolean useContactsDict, final boolean usePersonalizedDicts, final boolean forceReloadMainDictionary, final DictionaryInitializationListener listener, final String dictNamePrefix) { - final boolean localeHasBeenChanged = !newLocale.equals(mDictionaries.mLocale); - // We always try to have the main dictionary. Other dictionaries can be unused. - final boolean reloadMainDictionary = localeHasBeenChanged || forceReloadMainDictionary; + final HashMap<Locale, ArrayList<String>> existingDictsToCleanup = new HashMap<>(); // TODO: Make subDictTypesToUse configurable by resource or a static final list. final HashSet<String> subDictTypesToUse = new HashSet<>(); + subDictTypesToUse.add(Dictionary.TYPE_USER); if (useContactsDict) { subDictTypesToUse.add(Dictionary.TYPE_CONTACTS); } - subDictTypesToUse.add(Dictionary.TYPE_USER); if (usePersonalizedDicts) { subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); subDictTypesToUse.add(Dictionary.TYPE_PERSONALIZATION); subDictTypesToUse.add(Dictionary.TYPE_CONTEXTUAL); } - final Dictionary newMainDict; - if (reloadMainDictionary) { - // The main dictionary will be asynchronously loaded. - newMainDict = null; - } else { - newMainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN); - } - - final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); - for (final String dictType : SUB_DICT_TYPES) { - if (!subDictTypesToUse.contains(dictType)) { - // This dictionary will not be used. + // Gather all dictionaries. We'll remove them from the list to clean up later. + for (final Locale newLocale : newLocales) { + final ArrayList<String> dictsForLocale = new ArrayList<>(); + existingDictsToCleanup.put(newLocale, dictsForLocale); + final DictionaryGroup currentDictionaryGroupForLocale = + findDictionaryGroupWithLocale(mDictionaryGroups, newLocale); + if (null == currentDictionaryGroupForLocale) { continue; } - final ExpandableBinaryDictionary dict; - if (!localeHasBeenChanged && mDictionaries.hasDict(dictType)) { - // Continue to use current dictionary. - dict = mDictionaries.getSubDict(dictType); + for (final String dictType : SUB_DICT_TYPES) { + if (currentDictionaryGroupForLocale.hasDict(dictType)) { + dictsForLocale.add(dictType); + } + } + if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN)) { + dictsForLocale.add(Dictionary.TYPE_MAIN); + } + } + + final DictionaryGroup[] newDictionaryGroups = new DictionaryGroup[newLocales.length]; + for (int i = 0; i < newLocales.length; ++i) { + final Locale newLocale = newLocales[i]; + final DictionaryGroup dictionaryGroupForLocale = + findDictionaryGroupWithLocale(mDictionaryGroups, newLocale); + final ArrayList<String> dictsToCleanupForLocale = existingDictsToCleanup.get(newLocale); + final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale); + + final Dictionary mainDict; + if (forceReloadMainDictionary || noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN)) { + mainDict = null; } else { - // Start to use new dictionary. - dict = getSubDict(dictType, context, newLocale, null /* dictFile */, - dictNamePrefix); + mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN); + dictsToCleanupForLocale.remove(Dictionary.TYPE_MAIN); + } + + final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); + for (final String subDictType : subDictTypesToUse) { + final ExpandableBinaryDictionary subDict; + if (noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(subDictType)) { + // Create a new dictionary. + subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */, + dictNamePrefix); + } else { + // Reuse the existing dictionary, and don't close it at the end + subDict = dictionaryGroupForLocale.getSubDict(subDictType); + dictsToCleanupForLocale.remove(subDictType); + } + subDicts.put(subDictType, subDict); } - subDicts.put(dictType, dict); + newDictionaryGroups[i] = new DictionaryGroup(newLocale, mainDict, subDicts); } // Replace Dictionaries. - final Dictionaries newDictionaries = new Dictionaries(newLocale, newMainDict, subDicts); - final Dictionaries oldDictionaries; + final DictionaryGroup[] oldDictionaryGroups; synchronized (mLock) { - oldDictionaries = mDictionaries; - mDictionaries = newDictionaries; + oldDictionaryGroups = mDictionaryGroups; + mDictionaryGroups = newDictionaryGroups; + mMostProbableDictionaryGroup = newDictionaryGroups[0]; mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context); - if (reloadMainDictionary) { - asyncReloadMainDictionary(context, newLocale, listener); + if (hasAtLeastOneUninitializedMainDictionary()) { + asyncReloadUninitializedMainDictionaries(context, newLocales, listener); } } if (listener != null) { - listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary()); + listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary()); } + // Clean up old dictionaries. - if (reloadMainDictionary) { - oldDictionaries.closeDict(Dictionary.TYPE_MAIN); - } - for (final String dictType : SUB_DICT_TYPES) { - if (localeHasBeenChanged || !subDictTypesToUse.contains(dictType)) { - oldDictionaries.closeDict(dictType); + for (final Locale localeToCleanUp : existingDictsToCleanup.keySet()) { + final ArrayList<String> dictTypesToCleanUp = + existingDictsToCleanup.get(localeToCleanUp); + final DictionaryGroup dictionarySetToCleanup = + findDictionaryGroupWithLocale(oldDictionaryGroups, localeToCleanUp); + for (final String dictType : dictTypesToCleanUp) { + dictionarySetToCleanup.closeDict(dictType); } } - oldDictionaries.mSubDictMap.clear(); } - private void asyncReloadMainDictionary(final Context context, final Locale locale, - final DictionaryInitializationListener listener) { + private void asyncReloadUninitializedMainDictionaries(final Context context, + final Locale[] locales, final DictionaryInitializationListener listener) { final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); - mLatchForWaitingLoadingMainDictionary = latchForWaitingLoadingMainDictionary; + mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary; ExecutorUtils.getExecutor("InitializeBinaryDictionary").execute(new Runnable() { @Override public void run() { - final Dictionary mainDict = - DictionaryFactory.createMainDictionaryFromManager(context, locale); - synchronized (mLock) { - if (locale.equals(mDictionaries.mLocale)) { - mDictionaries.setMainDict(mainDict); - } else { - // Dictionary facilitator has been reset for another locale. - mainDict.close(); + for (final Locale locale : locales) { + final DictionaryGroup dictionaryGroup = + findDictionaryGroupWithLocale(mDictionaryGroups, locale); + if (null == dictionaryGroup) { + // This should never happen, but better safe than crashy + Log.w(TAG, "Expected a dictionary group for " + locale + " but none found"); + continue; + } + final Dictionary mainDict = + DictionaryFactory.createMainDictionaryFromManager(context, locale); + synchronized (mLock) { + if (locale.equals(dictionaryGroup.mLocale)) { + dictionaryGroup.setMainDict(mainDict); + } else { + // Dictionary facilitator has been reset for another locale. + mainDict.close(); + } } } if (listener != null) { - listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary()); + listener.onUpdateMainDictionaryAvailability( + hasAtLeastOneInitializedMainDictionary()); } latchForWaitingLoadingMainDictionary.countDown(); } @@ -313,82 +451,126 @@ public class DictionaryFacilitator { } @UsedForTesting - public void resetDictionariesForTesting(final Context context, final Locale locale, + public void resetDictionariesForTesting(final Context context, final Locale[] locales, final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles, final Map<String, Map<String, String>> additionalDictAttributes) { Dictionary mainDictionary = null; final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); - for (final String dictType : dictionaryTypes) { - if (dictType.equals(Dictionary.TYPE_MAIN)) { - mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context, locale); - } else { - final File dictFile = dictionaryFiles.get(dictType); - final ExpandableBinaryDictionary dict = getSubDict( - dictType, context, locale, dictFile, "" /* dictNamePrefix */); - if (additionalDictAttributes.containsKey(dictType)) { - dict.clearAndFlushDictionaryWithAdditionalAttributes( - additionalDictAttributes.get(dictType)); - } - if (dict == null) { - throw new RuntimeException("Unknown dictionary type: " + dictType); + final DictionaryGroup[] dictionaryGroups = new DictionaryGroup[locales.length]; + for (int i = 0; i < locales.length; ++i) { + final Locale locale = locales[i]; + for (final String dictType : dictionaryTypes) { + if (dictType.equals(Dictionary.TYPE_MAIN)) { + mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context, + locale); + } else { + final File dictFile = dictionaryFiles.get(dictType); + final ExpandableBinaryDictionary dict = getSubDict( + dictType, context, locale, dictFile, "" /* dictNamePrefix */); + if (additionalDictAttributes.containsKey(dictType)) { + dict.clearAndFlushDictionaryWithAdditionalAttributes( + additionalDictAttributes.get(dictType)); + } + if (dict == null) { + throw new RuntimeException("Unknown dictionary type: " + dictType); + } + dict.reloadDictionaryIfRequired(); + dict.waitAllTasksForTests(); + subDicts.put(dictType, dict); } - dict.reloadDictionaryIfRequired(); - dict.waitAllTasksForTests(); - subDicts.put(dictType, dict); } + dictionaryGroups[i] = new DictionaryGroup(locale, mainDictionary, subDicts); } - mDictionaries = new Dictionaries(locale, mainDictionary, subDicts); + mDictionaryGroups = dictionaryGroups; + mMostProbableDictionaryGroup = dictionaryGroups[0]; } public void closeDictionaries() { - final Dictionaries dictionaries; + final DictionaryGroup[] dictionaryGroups; synchronized (mLock) { - dictionaries = mDictionaries; - mDictionaries = new Dictionaries(); + dictionaryGroups = mDictionaryGroups; + mMostProbableDictionaryGroup = new DictionaryGroup(); + mDictionaryGroups = new DictionaryGroup[] { mMostProbableDictionaryGroup }; } - for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - dictionaries.closeDict(dictType); + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + dictionaryGroup.closeDict(dictType); + } } mDistracterFilter.close(); + if (mPersonalizationHelper != null) { + mPersonalizationHelper.close(); + } } @UsedForTesting public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { - return mDictionaries.getSubDict(dictName); + return mMostProbableDictionaryGroup.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); - return mainDict != null && mainDict.isInitialized(); + // The main dictionaries are loaded asynchronously. Don't cache the return value + // of these methods. + public boolean hasAtLeastOneInitializedMainDictionary() { + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN); + if (mainDict != null && mainDict.isInitialized()) { + return true; + } + } + return false; + } + + public boolean hasAtLeastOneUninitializedMainDictionary() { + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN); + if (mainDict == null || !mainDict.isInitialized()) { + return true; + } + } + return false; } public boolean hasPersonalizationDictionary() { - return mDictionaries.hasDict(Dictionary.TYPE_PERSONALIZATION); + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + if (dictionaryGroup.hasDict(Dictionary.TYPE_PERSONALIZATION)) { + return true; + } + } + return false; } public void flushPersonalizationDictionary() { - final ExpandableBinaryDictionary personalizationDict = - mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION); - if (personalizationDict != null) { - personalizationDict.asyncFlushBinaryDictionary(); - } + final HashSet<ExpandableBinaryDictionary> personalizationDictsUsedForSuggestion = + new HashSet<>(); + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + final ExpandableBinaryDictionary personalizationDictUsedForSuggestion = + dictionaryGroup.getSubDict(Dictionary.TYPE_PERSONALIZATION); + personalizationDictsUsedForSuggestion.add(personalizationDictUsedForSuggestion); + } + mPersonalizationHelper.flushPersonalizationDictionariesToUpdate( + personalizationDictsUsedForSuggestion); + mDistracterFilter.close(); } - public void waitForLoadingMainDictionary(final long timeout, final TimeUnit unit) + public void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit) throws InterruptedException { - mLatchForWaitingLoadingMainDictionary.await(timeout, unit); + mLatchForWaitingLoadingMainDictionaries.await(timeout, unit); } @UsedForTesting public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) throws InterruptedException { - waitForLoadingMainDictionary(timeout, unit); - final Map<String, ExpandableBinaryDictionary> dictMap = mDictionaries.mSubDictMap; - for (final ExpandableBinaryDictionary dict : dictMap.values()) { - dict.waitAllTasksForTests(); + waitForLoadingMainDictionaries(timeout, unit); + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + for (final ExpandableBinaryDictionary dict : dictionaryGroup.mSubDictMap.values()) { + dict.waitAllTasksForTests(); + } } } @@ -397,34 +579,35 @@ public class DictionaryFacilitator { } public void addWordToUserDictionary(final Context context, final String word) { - final Locale locale = getLocale(); + final Locale locale = getMostProbableLocale(); if (locale == null) { return; } + // TODO: add a toast telling what language this is being added to? UserBinaryDictionary.addWordToUserDictionary(context, locale, word); } public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, - final PrevWordsInfo prevWordsInfo, final int timeStampInSeconds, + final NgramContext ngramContext, final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { - final Dictionaries dictionaries = mDictionaries; + final DictionaryGroup dictionaryGroup = getDictionaryGroupForMostProbableLanguage(); final String[] words = suggestion.split(Constants.WORD_SEPARATOR); - PrevWordsInfo prevWordsInfoForCurrentWord = prevWordsInfo; + NgramContext ngramContextForCurrentWord = ngramContext; 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, ngramContextForCurrentWord, currentWord, wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive); - prevWordsInfoForCurrentWord = - prevWordsInfoForCurrentWord.getNextPrevWordsInfo(new WordInfo(currentWord)); + ngramContextForCurrentWord = + ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord)); } } - private void addWordToUserHistory(final Dictionaries dictionaries, - final PrevWordsInfo prevWordsInfo, final String word, final boolean wasAutoCapitalized, + private void addWordToUserHistory(final DictionaryGroup dictionaryGroup, + final NgramContext ngramContext, 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; } @@ -432,7 +615,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 */) @@ -453,8 +636,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) { @@ -467,14 +650,15 @@ public class DictionaryFacilitator { // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". // We don't add words with 0-frequency (assuming they would be profanity etc.). final boolean isValid = maxFreq > 0; - UserHistoryDictionary.addToDictionary(userHistoryDictionary, prevWordsInfo, secondWord, + UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, secondWord, isValid, timeStampInSeconds, new DistracterFilterCheckingIsInDictionary( mDistracterFilter, userHistoryDictionary)); } private void removeWord(final String dictName, final String word) { - final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); + final ExpandableBinaryDictionary dictionary = + getDictionaryGroupForMostProbableLanguage().getSubDict(dictName); if (dictionary != null) { dictionary.removeUnigramEntryDynamically(word); } @@ -488,23 +672,29 @@ public class DictionaryFacilitator { // TODO: Revise the way to fusion suggestion results. public SuggestionResults getSuggestionResults(final WordComposer composer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final NgramContext ngramContext, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) { - final Dictionaries dictionaries = mDictionaries; + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; final SuggestionResults suggestionResults = new SuggestionResults( - dictionaries.mLocale, SuggestedWords.MAX_SUGGESTIONS, - prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence); - 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); - if (null == dictionary) continue; - final ArrayList<SuggestedWordInfo> dictionarySuggestions = - dictionary.getSuggestions(composer, prevWordsInfo, proximityInfo, - settingsValuesForSuggestion, sessionId, languageWeight); - if (null == dictionarySuggestions) continue; - suggestionResults.addAll(dictionarySuggestions); - if (null != suggestionResults.mRawSuggestions) { - suggestionResults.mRawSuggestions.addAll(dictionarySuggestions); + SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext()); + final float[] weightOfLangModelVsSpatialModel = + new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL }; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + final Dictionary dictionary = dictionaryGroup.getDict(dictType); + if (null == dictionary) continue; + final float weightForLocale = composer.isBatchMode() + ? dictionaryGroup.mWeightForGesturingInLocale + : dictionaryGroup.mWeightForTypingInLocale; + final ArrayList<SuggestedWordInfo> dictionarySuggestions = + dictionary.getSuggestions(composer, ngramContext, proximityInfo, + settingsValuesForSuggestion, sessionId, + weightForLocale, weightOfLangModelVsSpatialModel); + if (null == dictionarySuggestions) continue; + suggestionResults.addAll(dictionarySuggestions); + if (null != suggestionResults.mRawSuggestions) { + suggestionResults.mRawSuggestions.addAll(dictionarySuggestions); + } } } return suggestionResults; @@ -514,20 +704,22 @@ public class DictionaryFacilitator { if (TextUtils.isEmpty(word)) { return false; } - final Dictionaries dictionaries = mDictionaries; - if (dictionaries.mLocale == null) { - return false; - } - final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); - for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - final Dictionary dictionary = dictionaries.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. - if (null == dictionary) continue; - if (dictionary.isValidWord(word) - || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) { - return true; + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + if (dictionaryGroup.mLocale == null) { + continue; + } + final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + 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. + if (null == dictionary) continue; + if (dictionary.isValidWord(word) + || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) { + return true; + } } } return false; @@ -539,18 +731,20 @@ public class DictionaryFacilitator { return Dictionary.NOT_A_PROBABILITY; } int maxFreq = Dictionary.NOT_A_PROBABILITY; - final Dictionaries dictionaries = mDictionaries; - for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { - final Dictionary dictionary = dictionaries.getDict(dictType); - if (dictionary == null) continue; - final int tempFreq; - if (isGettingMaxFrequencyOfExactMatches) { - tempFreq = dictionary.getMaxFrequencyOfExactMatches(word); - } else { - tempFreq = dictionary.getFrequency(word); - } - if (tempFreq >= maxFreq) { - maxFreq = tempFreq; + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { + final Dictionary dictionary = dictionaryGroup.getDict(dictType); + if (dictionary == null) continue; + final int tempFreq; + if (isGettingMaxFrequencyOfExactMatches) { + tempFreq = dictionary.getMaxFrequencyOfExactMatches(word); + } else { + tempFreq = dictionary.getFrequency(word); + } + if (tempFreq >= maxFreq) { + maxFreq = tempFreq; + } } } return maxFreq; @@ -565,9 +759,12 @@ public class DictionaryFacilitator { } private void clearSubDictionary(final String dictName) { - final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); - if (dictionary != null) { - dictionary.clear(); + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictName); + if (dictionary != null) { + dictionary.clear(); + } } } @@ -579,6 +776,7 @@ public class DictionaryFacilitator { // personalization dictionary. public void clearPersonalizationDictionary() { clearSubDictionary(Dictionary.TYPE_PERSONALIZATION); + mPersonalizationHelper.clearDictionariesToUpdate(); } public void clearContextualDictionary() { @@ -588,39 +786,23 @@ public class DictionaryFacilitator { public void addEntriesToPersonalizationDictionary( final PersonalizationDataChunk personalizationDataChunk, final SpacingAndPunctuations spacingAndPunctuations, - final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) { - final ExpandableBinaryDictionary personalizationDict = - mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION); - if (personalizationDict == null) { - if (callback != null) { - callback.onFinished(); - } - return; - } - final ArrayList<LanguageModelParam> languageModelParams = - LanguageModelParam.createLanguageModelParamsFrom( - personalizationDataChunk.mTokens, - personalizationDataChunk.mTimestampInSeconds, - this /* dictionaryFacilitator */, spacingAndPunctuations, - new DistracterFilterCheckingIsInDictionary( - mDistracterFilter, personalizationDict)); - if (languageModelParams == null || languageModelParams.isEmpty()) { - if (callback != null) { - callback.onFinished(); - } - return; - } - personalizationDict.addMultipleDictionaryEntriesDynamically(languageModelParams, callback); + final AddMultipleDictionaryEntriesCallback callback) { + mPersonalizationHelper.addEntriesToPersonalizationDictionariesToUpdate( + getMostProbableLocale(), personalizationDataChunk, spacingAndPunctuations, + callback); } + @UsedForTesting public void addPhraseToContextualDictionary(final String[] phrase, final int probability, final int bigramProbabilityForWords, final int bigramProbabilityForPhrases) { + // TODO: we're inserting the phrase into the dictionary for the active language. Rethink + // this a bit from a theoretical point of view. final ExpandableBinaryDictionary contextualDict = - mDictionaries.getSubDict(Dictionary.TYPE_CONTEXTUAL); + getDictionaryGroupForMostProbableLanguage().getSubDict(Dictionary.TYPE_CONTEXTUAL); if (contextualDict == null) { return; } - PrevWordsInfo prevWordsInfo = PrevWordsInfo.BEGINNING_OF_SENTENCE; + NgramContext ngramContext = NgramContext.BEGINNING_OF_SENTENCE; for (int i = 0; i < phrase.length; i++) { final String[] subPhrase = Arrays.copyOfRange(phrase, i /* start */, phrase.length); final String subPhraseStr = TextUtils.join(Constants.WORD_SEPARATOR, subPhrase); @@ -630,7 +812,7 @@ public class DictionaryFacilitator { false /* isNotAWord */, false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP, DistracterFilter.EMPTY_DISTRACTER_FILTER); - contextualDict.addNgramEntry(prevWordsInfo, subPhraseStr, + contextualDict.addNgramEntry(ngramContext, subPhraseStr, bigramProbabilityForPhrases, BinaryDictionary.NOT_A_VALID_TIMESTAMP); if (i < phrase.length - 1) { @@ -640,21 +822,37 @@ public class DictionaryFacilitator { false /* isNotAWord */, false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP, DistracterFilter.EMPTY_DISTRACTER_FILTER); - contextualDict.addNgramEntry(prevWordsInfo, phrase[i], + contextualDict.addNgramEntry(ngramContext, phrase[i], bigramProbabilityForWords, BinaryDictionary.NOT_A_VALID_TIMESTAMP); } - prevWordsInfo = - prevWordsInfo.getNextPrevWordsInfo(new PrevWordsInfo.WordInfo(phrase[i])); + ngramContext = + ngramContext.getNextNgramContext(new NgramContext.WordInfo(phrase[i])); } } public void dumpDictionaryForDebug(final String dictName) { - final ExpandableBinaryDictionary dictToDump = mDictionaries.getSubDict(dictName); - if (dictToDump == null) { - Log.e(TAG, "Cannot dump " + dictName + ". " - + "The dictionary is not being used for suggestion or cannot be dumped."); - return; + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + final ExpandableBinaryDictionary dictToDump = dictionaryGroup.getSubDict(dictName); + if (dictToDump == null) { + Log.e(TAG, "Cannot dump " + dictName + ". " + + "The dictionary is not being used for suggestion or cannot be dumped."); + return; + } + dictToDump.dumpAllWordsForDebug(); + } + } + + public ArrayList<Pair<String, DictionaryStats>> getStatsOfEnabledSubDicts() { + final ArrayList<Pair<String, DictionaryStats>> statsOfEnabledSubDicts = new ArrayList<>(); + final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; + for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { + for (final String dictType : SUB_DICT_TYPES) { + final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictType); + if (dictionary == null) continue; + statsOfEnabledSubDicts.add(new Pair<>(dictType, dictionary.getDictionaryStats())); + } } - dictToDump.dumpAllWordsForDebug(); + return statsOfEnabledSubDicts; } } diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java new file mode 100644 index 000000000..1b33d9129 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorLruCache.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import java.util.HashSet; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import com.android.inputmethod.annotations.UsedForTesting; + +import android.content.Context; +import android.util.Log; +import android.util.LruCache; + +/** + * Cache for dictionary facilitators of multiple locales. + * This class automatically creates and releases facilitator instances using LRU policy. + */ +public class DictionaryFacilitatorLruCache { + private static final String TAG = DictionaryFacilitatorLruCache.class.getSimpleName(); + private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000; + private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5; + + /** + * Class extends LruCache. This class tracks cached locales and closes evicted dictionaries by + * overriding entryRemoved. + */ + private static class DictionaryFacilitatorLruCacheInner extends + LruCache<Locale, DictionaryFacilitator> { + private final HashSet<Locale> mCachedLocales; + public DictionaryFacilitatorLruCacheInner(final HashSet<Locale> cachedLocales, + final int maxSize) { + super(maxSize); + mCachedLocales = cachedLocales; + } + + @Override + protected void entryRemoved(boolean evicted, Locale key, + DictionaryFacilitator oldValue, DictionaryFacilitator newValue) { + if (oldValue != null && oldValue != newValue) { + oldValue.closeDictionaries(); + } + if (key != null && newValue == null) { + // Remove locale from the cache when the dictionary facilitator for the locale is + // evicted and new facilitator is not set for the locale. + mCachedLocales.remove(key); + if (size() >= maxSize()) { + Log.w(TAG, "DictionaryFacilitator for " + key.toString() + + " has been evicted due to cache size limit." + + " size: " + size() + ", maxSize: " + maxSize()); + } + } + } + } + + private final Context mContext; + private final HashSet<Locale> mCachedLocales = new HashSet<>(); + private final String mDictionaryNamePrefix; + private final DictionaryFacilitatorLruCacheInner mLruCache; + private final Object mLock = new Object(); + private boolean mUseContactsDictionary = false; + + public DictionaryFacilitatorLruCache(final Context context, final int maxSize, + final String dictionaryNamePrefix) { + mContext = context; + mLruCache = new DictionaryFacilitatorLruCacheInner(mCachedLocales, maxSize); + mDictionaryNamePrefix = dictionaryNamePrefix; + } + + private void waitForLoadingMainDictionary(final DictionaryFacilitator dictionaryFacilitator) { + for (int i = 0; i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT; i++) { + try { + dictionaryFacilitator.waitForLoadingMainDictionaries( + WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS, TimeUnit.MILLISECONDS); + return; + } catch (final InterruptedException e) { + Log.i(TAG, "Interrupted during waiting for loading main dictionary.", e); + if (i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT - 1) { + Log.i(TAG, "Retry", e); + } else { + Log.w(TAG, "Give up retrying. Retried " + + MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT + " times.", e); + } + } + } + } + + private void resetDictionariesForLocaleLocked(final DictionaryFacilitator dictionaryFacilitator, + final Locale locale) { + dictionaryFacilitator.resetDictionariesWithDictNamePrefix(mContext, new Locale[] { locale }, + mUseContactsDictionary, false /* usePersonalizedDicts */, + false /* forceReloadMainDictionary */, null /* listener */, + mDictionaryNamePrefix); + } + + public void setUseContactsDictionary(final boolean useContectsDictionary) { + if (mUseContactsDictionary == useContectsDictionary) { + // The value has not been changed. + return; + } + synchronized (mLock) { + mUseContactsDictionary = useContectsDictionary; + for (final Locale locale : mCachedLocales) { + final DictionaryFacilitator dictionaryFacilitator = mLruCache.get(locale); + resetDictionariesForLocaleLocked(dictionaryFacilitator, locale); + waitForLoadingMainDictionary(dictionaryFacilitator); + } + } + } + + public DictionaryFacilitator get(final Locale locale) { + DictionaryFacilitator dictionaryFacilitator = mLruCache.get(locale); + if (dictionaryFacilitator != null) { + // dictionary falicitator for the locale is in the cache. + return dictionaryFacilitator; + } + synchronized (mLock) { + dictionaryFacilitator = mLruCache.get(locale); + if (dictionaryFacilitator != null) { + return dictionaryFacilitator; + } + dictionaryFacilitator = new DictionaryFacilitator(); + resetDictionariesForLocaleLocked(dictionaryFacilitator, locale); + waitForLoadingMainDictionary(dictionaryFacilitator); + mLruCache.put(locale, dictionaryFacilitator); + mCachedLocales.add(locale); + return dictionaryFacilitator; + } + } + + public void evictAll() { + synchronized (mLock) { + mLruCache.evictAll(); + mCachedLocales.clear(); + } + } + + @UsedForTesting + HashSet<Locale> getCachedLocalesForTesting() { + return mCachedLocales; + } +} diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java index 59de4f82a..3459b426d 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java @@ -50,7 +50,7 @@ public final class DictionaryFactory { final Locale locale, final boolean useFullEditDistance) { if (null == locale) { Log.e(TAG, "No locale defined for dictionary"); - return new DictionaryCollection(Dictionary.TYPE_MAIN, + return new DictionaryCollection(Dictionary.TYPE_MAIN, locale, createReadOnlyBinaryDictionary(context, locale)); } @@ -75,7 +75,7 @@ public final class DictionaryFactory { // If the list is empty, that means we should not use any dictionary (for example, the user // explicitly disabled the main dictionary), so the following is okay. dictList is never // null, but if for some reason it is, DictionaryCollection handles it gracefully. - return new DictionaryCollection(Dictionary.TYPE_MAIN, dictList); + return new DictionaryCollection(Dictionary.TYPE_MAIN, locale, dictList); } /** @@ -188,7 +188,7 @@ public final class DictionaryFactory { public static Dictionary createDictionaryForTest(final AssetFileAddress[] dictionaryList, final boolean useFullEditDistance, Locale locale) { final DictionaryCollection dictionaryCollection = - new DictionaryCollection(Dictionary.TYPE_MAIN); + new DictionaryCollection(Dictionary.TYPE_MAIN, locale); for (final AssetFileAddress address : dictionaryList) { final ReadOnlyBinaryDictionary readOnlyBinaryDictionary = new ReadOnlyBinaryDictionary( address.mFilename, address.mOffset, address.mLength, useFullEditDistance, diff --git a/java/src/com/android/inputmethod/latin/DictionaryStats.java b/java/src/com/android/inputmethod/latin/DictionaryStats.java new file mode 100644 index 000000000..5dd39d3d6 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryStats.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import java.io.File; +import java.util.Locale; + +public class DictionaryStats { + public static final int NOT_AN_ENTRY_COUNT = -1; + + public final Locale mLocale; + public final String mDictName; + public final String mDictFilePath; + public final long mDictFileSize; + + public final int mUnigramCount; + public final int mNgramCount; + // TODO: Add more members. + + public DictionaryStats(final Locale locale, final String dictName, final File dictFile, + final int unigramCount, final int ngramCount) { + mLocale = locale; + mDictName = dictName; + mDictFilePath = dictFile.getAbsolutePath(); + mDictFileSize = dictFile.length(); + mUnigramCount = unigramCount; + mNgramCount = ngramCount; + } +} diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index c11a220a4..d24f80a45 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -27,6 +27,7 @@ import com.android.inputmethod.latin.makedict.UnsupportedFormatException; import com.android.inputmethod.latin.makedict.WordProperty; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; +import com.android.inputmethod.latin.utils.AsyncResultHolder; import com.android.inputmethod.latin.utils.CombinedFormatUtils; import com.android.inputmethod.latin.utils.DistracterFilter; import com.android.inputmethod.latin.utils.ExecutorUtils; @@ -45,6 +46,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import javax.annotation.Nonnull; + /** * Abstract base class for an expandable dictionary that can be created and updated dynamically * during runtime. When updated it automatically generates a new binary dictionary to handle future @@ -86,9 +89,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ private final String mDictName; - /** Dictionary locale */ - private final Locale mLocale; - /** Dictionary file */ private final File mDictFile; @@ -137,10 +137,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ public ExpandableBinaryDictionary(final Context context, final String dictName, final Locale locale, final String dictType, final File dictFile) { - super(dictType); + super(dictType, locale); mDictName = dictName; mContext = context; - mLocale = locale; mDictFile = getDictFile(context, dictName, dictFile); mBinaryDictionary = null; mIsReloading = new AtomicBoolean(); @@ -160,23 +159,25 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } private void asyncExecuteTaskWithWriteLock(final Runnable task) { - asyncExecuteTaskWithLock(mLock.writeLock(), task); + asyncExecuteTaskWithLock(mLock.writeLock(), mDictName /* executorName */, task); } - private void asyncExecuteTaskWithLock(final Lock lock, final Runnable task) { - asyncPreCheckAndExecuteTaskWithLock(lock, null /* preCheckTask */, task); + private void asyncExecuteTaskWithLock(final Lock lock, final String executorName, + final Runnable task) { + asyncPreCheckAndExecuteTaskWithLock(lock, null /* preCheckTask */, executorName, task); } private void asyncPreCheckAndExecuteTaskWithWriteLock( final Callable<Boolean> preCheckTask, final Runnable task) { - asyncPreCheckAndExecuteTaskWithLock(mLock.writeLock(), preCheckTask, task); + asyncPreCheckAndExecuteTaskWithLock(mLock.writeLock(), preCheckTask, + mDictName /* executorName */, task); } // Execute task with lock when the result of preCheckTask is true or preCheckTask is null. private void asyncPreCheckAndExecuteTaskWithLock(final Lock lock, - final Callable<Boolean> preCheckTask, final Runnable task) { - ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { + final Callable<Boolean> preCheckTask, final String executorName, final Runnable task) { + ExecutorUtils.getExecutor(executorName).execute(new Runnable() { @Override public void run() { if (preCheckTask != null) { @@ -293,20 +294,16 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } } - /** - * Adds unigram information of a word to the dictionary. May overwrite an existing entry. - */ - public void addUnigramEntryWithCheckingDistracter(final String word, final int frequency, - final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, - final boolean isBlacklisted, final int timestamp, - final DistracterFilter distracterFilter) { + private void updateDictionaryWithWriteLockIfWordIsNotADistracter( + @Nonnull final Runnable updateTask, + @Nonnull final String word, @Nonnull final DistracterFilter distracterFilter) { reloadDictionaryIfRequired(); asyncPreCheckAndExecuteTaskWithWriteLock( new Callable<Boolean>() { @Override public Boolean call() throws Exception { return !distracterFilter.isDistracterToWordsInDictionaries( - PrevWordsInfo.EMPTY_PREV_WORDS_INFO, word, mLocale); + NgramContext.EMPTY_PREV_WORDS_INFO, word, mLocale); } }, new Runnable() { @@ -316,12 +313,27 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { return; } runGCIfRequiredLocked(true /* mindsBlockByGC */); - addUnigramLocked(word, frequency, shortcutTarget, shortcutFreq, - isNotAWord, isBlacklisted, timestamp); + updateTask.run(); } }); } + /** + * Adds unigram information of a word to the dictionary. May overwrite an existing entry. + */ + public void addUnigramEntryWithCheckingDistracter(final String word, final int frequency, + final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, + final boolean isBlacklisted, final int timestamp, + @Nonnull final DistracterFilter distracterFilter) { + updateDictionaryWithWriteLockIfWordIsNotADistracter(new Runnable() { + @Override + public void run() { + addUnigramLocked(word, frequency, shortcutTarget, shortcutFreq, + isNotAWord, isBlacklisted, timestamp); + } + }, word, distracterFilter); + } + protected void addUnigramLocked(final String word, final int frequency, final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, final boolean isBlacklisted, final int timestamp) { @@ -355,7 +367,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { /** * Adds n-gram information of a word to the dictionary. May overwrite an existing entry. */ - public void addNgramEntry(final PrevWordsInfo prevWordsInfo, final String word, + public void addNgramEntry(@Nonnull final NgramContext ngramContext, final String word, final int frequency, final int timestamp) { reloadDictionaryIfRequired(); asyncExecuteTaskWithWriteLock(new Runnable() { @@ -365,17 +377,17 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { return; } runGCIfRequiredLocked(true /* mindsBlockByGC */); - addNgramEntryLocked(prevWordsInfo, word, frequency, timestamp); + addNgramEntryLocked(ngramContext, word, frequency, timestamp); } }); } - protected void addNgramEntryLocked(final PrevWordsInfo prevWordsInfo, final String word, + protected void addNgramEntryLocked(@Nonnull final NgramContext ngramContext, final String word, final int frequency, final int timestamp) { - if (!mBinaryDictionary.addNgramEntry(prevWordsInfo, word, frequency, timestamp)) { + if (!mBinaryDictionary.addNgramEntry(ngramContext, word, frequency, timestamp)) { if (DEBUG) { Log.i(TAG, "Cannot add n-gram entry."); - Log.i(TAG, " PrevWordsInfo: " + prevWordsInfo + ", word: " + word); + Log.i(TAG, " NgramContext: " + ngramContext + ", word: " + word); } } } @@ -384,7 +396,8 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * Dynamically remove the n-gram entry in the dictionary. */ @UsedForTesting - public void removeNgramDynamically(final PrevWordsInfo prevWordsInfo, final String word) { + public void removeNgramDynamically(@Nonnull final NgramContext ngramContext, + final String word) { reloadDictionaryIfRequired(); asyncExecuteTaskWithWriteLock(new Runnable() { @Override @@ -393,16 +406,36 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { return; } runGCIfRequiredLocked(true /* mindsBlockByGC */); - if (!mBinaryDictionary.removeNgramEntry(prevWordsInfo, word)) { + if (!mBinaryDictionary.removeNgramEntry(ngramContext, word)) { if (DEBUG) { Log.i(TAG, "Cannot remove n-gram entry."); - Log.i(TAG, " PrevWordsInfo: " + prevWordsInfo + ", word: " + word); + Log.i(TAG, " NgramContext: " + ngramContext + ", word: " + word); } } } }); } + /** + * Update dictionary for the word with the ngramContext if the word is not a distracter. + */ + public void updateEntriesForWordWithCheckingDistracter(@Nonnull final NgramContext ngramContext, + final String word, final boolean isValidWord, final int count, final int timestamp, + @Nonnull final DistracterFilter distracterFilter) { + updateDictionaryWithWriteLockIfWordIsNotADistracter(new Runnable() { + @Override + public void run() { + if (!mBinaryDictionary.updateEntriesForWordWithNgramContext(ngramContext, word, + isValidWord, count, timestamp)) { + if (DEBUG) { + Log.e(TAG, "Cannot update counter. word: " + word + + " context: "+ ngramContext.toString()); + } + } + } + }, word, distracterFilter); + } + public interface AddMultipleDictionaryEntriesCallback { public void onFinished(); } @@ -411,7 +444,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * Dynamically add multiple entries to the dictionary. */ public void addMultipleDictionaryEntriesDynamically( - final ArrayList<LanguageModelParam> languageModelParams, + @Nonnull final ArrayList<LanguageModelParam> languageModelParams, final AddMultipleDictionaryEntriesCallback callback) { reloadDictionaryIfRequired(); asyncExecuteTaskWithWriteLock(new Runnable() { @@ -435,9 +468,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final NgramContext ngramContext, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId, - final float[] inOutLanguageWeight) { + final float weightForLocale, final float[] inOutWeightOfLangModelVsSpatialModel) { reloadDictionaryIfRequired(); boolean lockAcquired = false; try { @@ -448,8 +481,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { return null; } final ArrayList<SuggestedWordInfo> suggestions = - mBinaryDictionary.getSuggestions(composer, prevWordsInfo, proximityInfo, - settingsValuesForSuggestion, sessionId, inOutLanguageWeight); + mBinaryDictionary.getSuggestions(composer, ngramContext, proximityInfo, + settingsValuesForSuggestion, sessionId, weightForLocale, + inOutWeightOfLangModelVsSpatialModel); if (mBinaryDictionary.isCorrupted()) { Log.i(TAG, "Dictionary (" + mDictName +") is corrupted. " + "Remove and regenerate it."); @@ -519,9 +553,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { } - protected boolean isValidNgramLocked(final PrevWordsInfo prevWordsInfo, final String word) { + protected boolean isValidNgramLocked(final NgramContext ngramContext, final String word) { if (mBinaryDictionary == null) return false; - return mBinaryDictionary.isValidNgram(prevWordsInfo, word); + return mBinaryDictionary.isValidNgram(ngramContext, word); } /** @@ -644,10 +678,45 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { }); } + private static int parseEntryCount(final String entryCountStr) { + int entryCount; + try { + entryCount = Integer.parseInt(entryCountStr); + } catch (final NumberFormatException e) { + entryCount = DictionaryStats.NOT_AN_ENTRY_COUNT; + } + return entryCount; + } + + public DictionaryStats getDictionaryStats() { + reloadDictionaryIfRequired(); + final AsyncResultHolder<DictionaryStats> result = new AsyncResultHolder<>(); + asyncExecuteTaskWithLock(mLock.readLock(), mDictName /* executorName */, new Runnable() { + @Override + public void run() { + if (mBinaryDictionary == null) { + result.set(new DictionaryStats(mLocale, mDictName, mDictFile, + DictionaryStats.NOT_AN_ENTRY_COUNT, + DictionaryStats.NOT_AN_ENTRY_COUNT)); + } + final int unigramCount = parseEntryCount( + mBinaryDictionary.getPropertyForGettingStats( + BinaryDictionary.MAX_UNIGRAM_COUNT_QUERY)); + // TODO: Get dedicated entry counts for bigram, trigram, and so on. + final int ngramCount = parseEntryCount(mBinaryDictionary.getPropertyForGettingStats( + BinaryDictionary.MAX_BIGRAM_COUNT_QUERY)); + // TODO: Get more information from dictionary. + result.set(new DictionaryStats(mLocale, mDictName, mDictFile, unigramCount, + ngramCount)); + } + }); + return result.get(null /* defaultValue */, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS); + } + @UsedForTesting public void waitAllTasksForTests() { final CountDownLatch countDownLatch = new CountDownLatch(1); - ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { + asyncExecuteTaskWithWriteLock(new Runnable() { @Override public void run() { countDownLatch.countDown(); @@ -669,10 +738,10 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { public void dumpAllWordsForDebug() { reloadDictionaryIfRequired(); - asyncExecuteTaskWithLock(mLock.readLock(), new Runnable() { + asyncExecuteTaskWithLock(mLock.readLock(), "dumpAllWordsForDebug", new Runnable() { @Override public void run() { - Log.d(TAG, "Dump dictionary: " + mDictName); + Log.d(TAG, "Dump dictionary: " + mDictName + " for " + mLocale); try { final DictionaryHeader header = mBinaryDictionary.getHeader(); Log.d(TAG, "Format version: " + mBinaryDictionary.getFormatVersion()); diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java index fecb0ef94..ffd363b5d 100644 --- a/java/src/com/android/inputmethod/latin/InputAttributes.java +++ b/java/src/com/android/inputmethod/latin/InputAttributes.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin; +import static com.android.inputmethod.latin.Constants.ImeOption.NO_FLOATING_GESTURE_PREVIEW; import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE; import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; @@ -42,6 +43,12 @@ public final class InputAttributes { final public boolean mApplicationSpecifiedCompletionOn; final public boolean mShouldInsertSpacesAutomatically; final public boolean mShouldShowVoiceInputKey; + /** + * Whether the floating gesture preview should be disabled. If true, this should override the + * corresponding keyboard settings preference, always suppressing the floating preview text. + * {@link com.android.inputmethod.latin.settings.SettingsValues#mGestureFloatingPreviewTextEnabled} + */ + final public boolean mDisableGestureFloatingPreviewText; final public boolean mIsGeneralTextInput; final private int mInputType; final private EditorInfo mEditorInfo; @@ -77,6 +84,7 @@ public final class InputAttributes { mApplicationSpecifiedCompletionOn = false; mShouldInsertSpacesAutomatically = false; mShouldShowVoiceInputKey = false; + mDisableGestureFloatingPreviewText = false; mIsGeneralTextInput = false; return; } @@ -109,6 +117,9 @@ public final class InputAttributes { || hasNoMicrophoneKeyOption(); mShouldShowVoiceInputKey = !noMicrophone; + mDisableGestureFloatingPreviewText = InputAttributes.inPrivateImeOptions( + mPackageNameForPrivateImeOptions, NO_FLOATING_GESTURE_PREVIEW, editorInfo); + // If it's a browser edit field and auto correct is not ON explicitly, then // disable auto correction, but keep suggestions on. // If NO_SUGGESTIONS is set, don't do prediction. diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java index 790e0d830..d57a881c0 100644 --- a/java/src/com/android/inputmethod/latin/InputPointers.java +++ b/java/src/com/android/inputmethod/latin/InputPointers.java @@ -145,6 +145,12 @@ public final class InputPointers { return mPointerIds.getPrimitiveArray(); } + /** + * Gets the time each point was registered, in milliseconds, relative to the first event in the + * sequence. + * @return The time each point was registered, in milliseconds, relative to the first event in + * the sequence. + */ public int[] getTimes() { if (DebugFlags.DEBUG_ENABLED || DEBUG_TIME) { if (!isValidTimeStamps()) { diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java index 8cbf8379b..f3f736fbc 100644 --- a/java/src/com/android/inputmethod/latin/LastComposedWord.java +++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java @@ -48,7 +48,7 @@ public final class LastComposedWord { public final String mTypedWord; public final CharSequence mCommittedWord; public final String mSeparatorString; - public final PrevWordsInfo mPrevWordsInfo; + public final NgramContext mNgramContext; public final int mCapitalizedMode; public final InputPointers mInputPointers = new InputPointers(Constants.DICTIONARY_MAX_WORD_LENGTH); @@ -64,7 +64,7 @@ public final class LastComposedWord { public LastComposedWord(final ArrayList<Event> events, final InputPointers inputPointers, final String typedWord, final CharSequence committedWord, final String separatorString, - final PrevWordsInfo prevWordsInfo, final int capitalizedMode) { + final NgramContext ngramContext, final int capitalizedMode) { if (inputPointers != null) { mInputPointers.copy(inputPointers); } @@ -73,7 +73,7 @@ public final class LastComposedWord { mCommittedWord = committedWord; mSeparatorString = separatorString; mActive = true; - mPrevWordsInfo = prevWordsInfo; + mNgramContext = ngramContext; mCapitalizedMode = capitalizedMode; } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index d57db8e9a..f968a6c52 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -32,6 +32,7 @@ import android.content.res.Resources; import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.net.ConnectivityManager; +import android.os.Build; import android.os.Debug; import android.os.IBinder; import android.os.Message; @@ -59,6 +60,8 @@ import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; +import com.android.inputmethod.compat.ViewOutlineProviderCompatUtils; +import com.android.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; import com.android.inputmethod.event.Event; import com.android.inputmethod.event.HardwareEventDecoder; @@ -84,27 +87,31 @@ import com.android.inputmethod.latin.settings.SettingsActivity; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.suggestions.SuggestionStripView; import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; +import com.android.inputmethod.latin.touchinputconsumer.GestureConsumer; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.CapsModeUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; import com.android.inputmethod.latin.utils.CursorAnchorInfoUtils; import com.android.inputmethod.latin.utils.DialogUtils; -import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions; import com.android.inputmethod.latin.utils.ImportantNoticeUtils; import com.android.inputmethod.latin.utils.IntentUtils; import com.android.inputmethod.latin.utils.JniUtils; import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; import com.android.inputmethod.latin.utils.StatsUtils; +import com.android.inputmethod.latin.utils.StatsUtilsManager; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import com.android.inputmethod.latin.utils.ViewLayoutUtils; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; + /** * Input method implementation for Qwerty'ish keyboard. */ @@ -117,12 +124,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private static boolean DEBUG = false; private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; - - private static final int PENDING_IMS_CALLBACK_DURATION = 800; - - private static final int DELAY_WAIT_FOR_DICTIONARY_LOAD = 2000; // 2s - private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2; + private static final int PENDING_IMS_CALLBACK_DURATION_MILLIS = 800; + private static final long DELAY_WAIT_FOR_DICTIONARY_LOAD_MILLIS = TimeUnit.SECONDS.toMillis(2); + private static final long DELAY_DEALLOCATE_MEMORY_MILLIS = TimeUnit.SECONDS.toMillis(10); /** * The name of the scheme used by the Package Manager to warn of a new package installation, @@ -132,8 +137,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private final Settings mSettings; private final DictionaryFacilitator mDictionaryFacilitator = - new DictionaryFacilitator( - new DistracterFilterCheckingExactMatchesAndSuggestions(this /* context */)); + new DictionaryFacilitator(this /* context */); // TODO: Move from LatinIME. private final PersonalizationDictionaryUpdater mPersonalizationDictionaryUpdater = new PersonalizationDictionaryUpdater(this /* context */, mDictionaryFacilitator); @@ -153,6 +157,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: Move these {@link View}s to {@link KeyboardSwitcher}. private View mInputView; + private InsetsUpdater mInsetsUpdater; private SuggestionStripView mSuggestionStripView; private TextView mExtractEditText; @@ -161,6 +166,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private final SubtypeSwitcher mSubtypeSwitcher; private final SubtypeState mSubtypeState = new SubtypeState(); private final SpecialKeyDetector mSpecialKeyDetector; + private StatsUtilsManager mStatsUtilsManager; // Working variable for {@link #startShowingInputView()} and // {@link #onEvaluateInputViewShown()}. private boolean mIsExecutingStartShowingInputView; @@ -176,6 +182,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private final boolean mIsHardwareAcceleratedDrawingEnabled; + private GestureConsumer mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER; + public final UIHandler mHandler = new UIHandler(this); public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> { @@ -188,8 +196,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private static final int MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED = 6; private static final int MSG_RESET_CACHES = 7; private static final int MSG_WAIT_FOR_DICTIONARY_LOAD = 8; + private static final int MSG_DEALLOCATE_MEMORY = 9; // Update this when adding new messages - private static final int MSG_LAST = MSG_WAIT_FOR_DICTIONARY_LOAD; + private static final int MSG_LAST = MSG_DEALLOCATE_MEMORY; private static final int ARG1_NOT_GESTURE_INPUT = 0; private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; @@ -201,7 +210,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private int mDelayInMillisecondsToUpdateSuggestions; private int mDelayInMillisecondsToUpdateShiftState; - public UIHandler(final LatinIME ownerInstance) { + public UIHandler(@Nonnull final LatinIME ownerInstance) { super(ownerInstance); } @@ -246,19 +255,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen case MSG_RESUME_SUGGESTIONS: latinIme.mInputLogic.restartSuggestionsOnWordTouchedByCursor( latinIme.mSettings.getCurrent(), - msg.arg1 == ARG1_TRUE /* shouldIncludeResumedWordInSuggestions */, latinIme.mKeyboardSwitcher.getCurrentKeyboardScriptId()); break; case MSG_REOPEN_DICTIONARIES: // We need to re-evaluate the currently composing word in case the script has // changed. postWaitForDictionaryLoad(); - latinIme.resetSuggest(); + latinIme.resetDictionaryFacilitatorIfNecessary(); break; case MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED: + final SuggestedWords suggestedWords = (SuggestedWords) msg.obj; latinIme.mInputLogic.onUpdateTailBatchInputCompleted( latinIme.mSettings.getCurrent(), - (SuggestedWords) msg.obj, latinIme.mKeyboardSwitcher); + suggestedWords, latinIme.mKeyboardSwitcher); + latinIme.onTailBatchInputResultShown(suggestedWords); break; case MSG_RESET_CACHES: final SettingsValues settingsValues = latinIme.mSettings.getCurrent(); @@ -275,6 +285,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen case MSG_WAIT_FOR_DICTIONARY_LOAD: Log.i(TAG, "Timeout waiting for dictionary load"); break; + case MSG_DEALLOCATE_MEMORY: + latinIme.deallocateMemory(); + break; } } @@ -287,8 +300,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES)); } - public void postResumeSuggestions(final boolean shouldIncludeResumedWordInSuggestions, - final boolean shouldDelay) { + public void postResumeSuggestions(final boolean shouldDelay) { final LatinIME latinIme = getOwnerInstance(); if (latinIme == null) { return; @@ -298,13 +310,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } removeMessages(MSG_RESUME_SUGGESTIONS); if (shouldDelay) { - sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS, - shouldIncludeResumedWordInSuggestions ? ARG1_TRUE : ARG1_FALSE, - 0 /* ignored */), mDelayInMillisecondsToUpdateSuggestions); + sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), + mDelayInMillisecondsToUpdateSuggestions); } else { - sendMessage(obtainMessage(MSG_RESUME_SUGGESTIONS, - shouldIncludeResumedWordInSuggestions ? ARG1_TRUE : ARG1_FALSE, - 0 /* ignored */)); + sendMessage(obtainMessage(MSG_RESUME_SUGGESTIONS)); } } @@ -316,7 +325,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void postWaitForDictionaryLoad() { sendMessageDelayed(obtainMessage(MSG_WAIT_FOR_DICTIONARY_LOAD), - DELAY_WAIT_FOR_DICTIONARY_LOAD); + DELAY_WAIT_FOR_DICTIONARY_LOAD_MILLIS); } public void cancelWaitForDictionaryLoad() { @@ -345,6 +354,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mDelayInMillisecondsToUpdateShiftState); } + public void postDeallocateMemory() { + sendMessageDelayed(obtainMessage(MSG_DEALLOCATE_MEMORY), + DELAY_DEALLOCATE_MEMORY_MILLIS); + } + + public void cancelDeallocateMemory() { + removeMessages(MSG_DEALLOCATE_MEMORY); + } + + public boolean hasPendingDeallocateMemory() { + return hasMessages(MSG_DEALLOCATE_MEMORY); + } + @UsedForTesting public void removeAllMessages() { for (int i = 0; i <= MSG_LAST; ++i) { @@ -442,7 +464,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mPendingSuccessiveImsCallback = false; resetPendingImsCallback(); sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), - PENDING_IMS_CALLBACK_DURATION); + PENDING_IMS_CALLBACK_DURATION_MILLIS); } final LatinIME latinIme = getOwnerInstance(); if (latinIme != null) { @@ -450,6 +472,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen latinIme.onStartInputViewInternal(editorInfo, restarting); mAppliedEditorInfo = editorInfo; } + cancelDeallocateMemory(); } } @@ -463,6 +486,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen latinIme.onFinishInputViewInternal(finishingInput); mAppliedEditorInfo = null; } + if (!hasPendingDeallocateMemory()) { + postDeallocateMemory(); + } } } @@ -519,6 +545,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mKeyboardSwitcher = KeyboardSwitcher.getInstance(); mSpecialKeyDetector = new SpecialKeyDetector(this); + mStatsUtilsManager = StatsUtilsManager.getInstance(); mIsHardwareAcceleratedDrawingEnabled = InputMethodServiceCompatUtils.enableHardwareAcceleration(this); Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled); @@ -534,16 +561,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen KeyboardSwitcher.init(this); AudioAndHapticFeedbackManager.init(this); AccessibilityUtils.init(this); - StatsUtils.init(this); - + mStatsUtilsManager.onCreate(this /* context */); super.onCreate(); mHandler.onCreate(); DEBUG = DebugFlags.DEBUG_ENABLED; - // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}. + // TODO: Resolve mutual dependencies of {@link #loadSettings()} and + // {@link #resetDictionaryFacilitatorIfNecessary()}. loadSettings(); - resetSuggest(); + mSubtypeSwitcher.onSubtypeChanged(mRichImm.getCurrentRawSubtype()); + resetDictionaryFacilitatorIfNecessary(); // Register to receive ringer mode change and network state change. // Also receive installation and removal of a dictionary pack. @@ -567,37 +595,38 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen registerReceiver(mDictionaryDumpBroadcastReceiver, dictDumpFilter); DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this); - - StatsUtils.onCreate(mSettings.getCurrent()); + StatsUtils.onCreate(mSettings.getCurrent(), mRichImm); } // Has to be package-visible for unit tests @UsedForTesting void loadSettings() { - final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); + final Locale[] locales = mSubtypeSwitcher.getCurrentSubtypeLocales(); final EditorInfo editorInfo = getCurrentInputEditorInfo(); final InputAttributes inputAttributes = new InputAttributes( editorInfo, isFullscreenMode(), getPackageName()); - mSettings.loadSettings(this, locale, inputAttributes); + // TODO: pass the array instead + mSettings.loadSettings(this, locales[0], inputAttributes); final SettingsValues currentSettingsValues = mSettings.getCurrent(); AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(currentSettingsValues); // This method is called on startup and language switch, before the new layout has // been displayed. Opening dictionaries never affects responsivity as dictionaries are // asynchronously loaded. if (!mHandler.hasPendingReopenDictionaries()) { - resetSuggestForLocale(locale); + resetDictionaryFacilitatorForLocale(locales); } mDictionaryFacilitator.updateEnabledSubtypes(mRichImm.getMyEnabledInputMethodSubtypeList( true /* allowsImplicitlySelectedSubtypes */)); refreshPersonalizationDictionarySession(currentSettingsValues); - StatsUtils.onLoadSettings(currentSettingsValues); + mStatsUtilsManager.onLoadSettings(currentSettingsValues); } private void refreshPersonalizationDictionarySession( final SettingsValues currentSettingsValues) { - mPersonalizationDictionaryUpdater.onLoadSettings( - currentSettingsValues.mUsePersonalizedDicts, + mDictionaryFacilitator.setIsMonolingualUser( mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes()); + mPersonalizationDictionaryUpdater.onLoadSettings( + currentSettingsValues.mUsePersonalizedDicts); mContextualDictionaryUpdater.onLoadSettings(currentSettingsValues.mUsePersonalizedDicts); final boolean shouldKeepUserHistoryDictionaries; if (currentSettingsValues.mUsePersonalizedDicts) { @@ -621,38 +650,38 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } if (mHandler.hasPendingWaitForDictionaryLoad()) { mHandler.cancelWaitForDictionaryLoad(); - mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */, - false /* shouldDelay */); + mHandler.postResumeSuggestions(false /* shouldDelay */); } } - private void resetSuggest() { - final Locale switcherSubtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); - final String switcherLocaleStr = switcherSubtypeLocale.toString(); - final Locale subtypeLocale; - if (TextUtils.isEmpty(switcherLocaleStr)) { + private void resetDictionaryFacilitatorIfNecessary() { + final Locale[] subtypeSwitcherLocales = mSubtypeSwitcher.getCurrentSubtypeLocales(); + if (mDictionaryFacilitator.isForLocales(subtypeSwitcherLocales)) { + return; + } + final Locale[] subtypeLocales; + if (0 == subtypeSwitcherLocales.length) { // This happens in very rare corner cases - for example, immediately after a switch // to LatinIME has been requested, about a frame later another switch happens. In this // case, we are about to go down but we still don't know it, however the system tells - // us there is no current subtype so the locale is the empty string. Take the best - // possible guess instead -- it's bound to have no consequences, and we have no way - // of knowing anyway. + // us there is no current subtype. Log.e(TAG, "System is reporting no current subtype."); - subtypeLocale = getResources().getConfiguration().locale; + subtypeLocales = new Locale[] { getResources().getConfiguration().locale }; } else { - subtypeLocale = switcherSubtypeLocale; + subtypeLocales = subtypeSwitcherLocales; } - resetSuggestForLocale(subtypeLocale); + resetDictionaryFacilitatorForLocale(subtypeLocales); } /** - * Reset suggest by loading dictionaries for the locale and the current settings values. + * Reset the facilitator by loading dictionaries for the locales and the current settings values * - * @param locale the locale + * @param locales the locales */ - private void resetSuggestForLocale(final Locale locale) { + // TODO: make sure the current settings always have the right locales, and read from them + private void resetDictionaryFacilitatorForLocale(final Locale[] locales) { final SettingsValues settingsValues = mSettings.getCurrent(); - mDictionaryFacilitator.resetDictionaries(this /* context */, locale, + mDictionaryFacilitator.resetDictionaries(this /* context */, locales, settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts, false /* forceReloadMainDictionary */, this); if (settingsValues.mAutoCorrectionEnabledPerUserSettings) { @@ -667,7 +696,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen /* package private */ void resetSuggestMainDict() { final SettingsValues settingsValues = mSettings.getCurrent(); mDictionaryFacilitator.resetDictionaries(this /* context */, - mDictionaryFacilitator.getLocale(), settingsValues.mUseContactsDict, + mDictionaryFacilitator.getLocales(), settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts, true /* forceReloadMainDictionary */, this); } @@ -680,7 +709,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen unregisterReceiver(mConnectivityAndRingerModeChangeReceiver); unregisterReceiver(mDictionaryPackInstallReceiver); unregisterReceiver(mDictionaryDumpBroadcastReceiver); - StatsUtils.onDestroy(); + mStatsUtilsManager.onDestroy(); super.onDestroy(); } @@ -715,15 +744,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen cleanupInternalStateForFinishInput(); } } - // TODO: Remove this test. - if (!conf.locale.equals(mPersonalizationDictionaryUpdater.getLocale())) { - refreshPersonalizationDictionarySession(settingsValues); - } super.onConfigurationChanged(conf); } @Override public View onCreateInputView() { + StatsUtils.onCreateInputView(); return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled); } @@ -731,6 +757,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void setInputView(final View view) { super.setInputView(view); mInputView = view; + mInsetsUpdater = ViewOutlineProviderCompatUtils.setInsetsOutlineProvider(view); + updateSoftInputWindowLayoutParameters(); mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view); if (hasSuggestionStripView()) { mSuggestionStripView.setListener(this, view); @@ -752,12 +780,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen if (prevExtractEditText == nextExtractEditText) { return; } - if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && prevExtractEditText != null) { + if (prevExtractEditText != null) { prevExtractEditText.getViewTreeObserver().removeOnPreDrawListener( mExtractTextViewPreDrawListener); } mExtractEditText = nextExtractEditText; - if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK && mExtractEditText != null) { + if (mExtractEditText != null) { mExtractEditText.getViewTreeObserver().addOnPreDrawListener( mExtractTextViewPreDrawListener); } @@ -767,20 +795,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { - onExtractTextViewPreDraw(); + // CursorAnchorInfo is used on L and later. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.L) { + if (isFullscreenMode() && mExtractEditText != null) { + mInputLogic.onUpdateCursorAnchorInfo( + CursorAnchorInfoUtils.extractFromTextView(mExtractEditText)); + } + } return true; } }; - private void onExtractTextViewPreDraw() { - if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || !isFullscreenMode() - || mExtractEditText == null) { - return; - } - final CursorAnchorInfo info = CursorAnchorInfoUtils.getCursorAnchorInfo(mExtractEditText); - mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); - } - @Override public void setCandidatesView(final View view) { // To ensure that CandidatesView will never be set. @@ -795,11 +820,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) { mHandler.onStartInputView(editorInfo, restarting); + mStatsUtilsManager.onStartInputView(); } @Override public void onFinishInputView(final boolean finishingInput) { + StatsUtils.onFinishInputView(); mHandler.onFinishInputView(finishingInput); + mStatsUtilsManager.onFinishInputView(); + mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER; } @Override @@ -824,6 +853,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @SuppressWarnings("deprecation") private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) { super.onStartInputView(editorInfo, restarting); + // Switch to the null consumer to handle cases leading to early exit below, for which we + // also wouldn't be consuming gesture data. + mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER; mRichImm.clearSubtypeCaches(); final KeyboardSwitcher switcher = mKeyboardSwitcher; switcher.updateKeyboardTheme(); @@ -867,6 +899,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } + // Update to a gesture consumer with the current editor and IME state. + mGestureConsumer = GestureConsumer.newInstance(editorInfo, + mInputLogic.getPrivateCommandPerformer(), + Arrays.asList(mSubtypeSwitcher.getCurrentSubtypeLocales()), + switcher.getKeyboard()); + // Forward this event to the accessibility utilities, if enabled. final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance(); if (accessUtils.isTouchExplorationEnabled()) { @@ -875,6 +913,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo); final boolean isDifferentTextField = !restarting || inputTypeChanged; + + StatsUtils.onStartInputView(editorInfo.inputType, + Settings.getInstance().getCurrent().mDisplayOrientation, + !isDifferentTextField); + if (isDifferentTextField) { mSubtypeSwitcher.updateParametersOnStartInputView(); } @@ -900,12 +943,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen 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(); - } + resetDictionaryFacilitatorIfNecessary(); // TODO[IL]: Can the following be moved to InputLogic#startInput? if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( @@ -919,11 +957,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // 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. + // When rotating, and when input is starting again in a field from where the focus + // didn't move (the keyboard having been closed with the back key), + // initialSelStart and initialSelEnd sometimes are lying. Make a best effort to + // work around this bug. mInputLogic.mConnection.tryFixLyingCursorPosition(); - mHandler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */, - true /* shouldDelay */); + mHandler.postResumeSuggestions(true /* shouldDelay */); needToCallLoadKeyboardLater = false; } } else { @@ -970,7 +1009,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mHandler.cancelUpdateSuggestionStrip(); mainKeyboardView.setMainDictionaryAvailability( - mDictionaryFacilitator.hasInitializedMainDictionary()); + mDictionaryFacilitator.hasAtLeastOneInitializedMainDictionary()); mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn, currentSettingsValues.mKeyPreviewPopupDismissDelay); mainKeyboardView.setSlidingKeyInputPreviewEnabled( @@ -1009,13 +1048,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } private void cleanupInternalStateForFinishInput() { - mKeyboardSwitcher.deallocateMemory(); // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestionStrip(); // Should do the following in onFinishInputInternal but until JB MR2 it's not called :( mInputLogic.finishInput(); } + protected void deallocateMemory() { + mKeyboardSwitcher.deallocateMemory(); + } + @Override public void onUpdateSelection(final int oldSelStart, final int oldSelEnd, final int newSelStart, final int newSelEnd, @@ -1043,10 +1085,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // We cannot mark this method as @Override until new SDK becomes publicly available. // @Override public void onUpdateCursorAnchorInfo(final CursorAnchorInfo info) { - if (!ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK || isFullscreenMode()) { + if (isFullscreenMode()) { return; } - mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.fromObject(info)); + mInputLogic.onUpdateCursorAnchorInfo(CursorAnchorInfoCompatWrapper.wrap(info)); } /** @@ -1131,6 +1173,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onComputeInsets(final InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); + // This method may be called before {@link #setInputView(View)}. + if (mInputView == null) { + return; + } final SettingsValues settingsValues = mSettings.getCurrent(); final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); if (visibleKeyboardView == null || !hasSuggestionStripView()) { @@ -1143,6 +1189,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // no visual element will be shown on the screen. outInsets.touchableInsets = inputHeight; outInsets.visibleTopInsets = inputHeight; + mInsetsUpdater.setInsets(outInsets); return; } final int suggestionsHeight = (!mKeyboardSwitcher.isShowingEmojiPalettes() @@ -1163,14 +1210,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } outInsets.contentTopInsets = visibleTopY; outInsets.visibleTopInsets = visibleTopY; + mInsetsUpdater.setInsets(outInsets); } - public void startShowingInputView() { + public void startShowingInputView(final boolean needsToLoadKeyboard) { mIsExecutingStartShowingInputView = true; // This {@link #showWindow(boolean)} will eventually call back // {@link #onEvaluateInputViewShown()}. showWindow(true /* showInput */); mIsExecutingStartShowingInputView = false; + if (needsToLoadKeyboard) { + loadKeyboard(); + } } public void stopShowingInputView() { @@ -1178,6 +1229,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override + public boolean onShowInputRequested(final int flags, final boolean configChange) { + if (Settings.getInstance().getCurrent().mHasHardwareKeyboard) { + return true; + } + return super.onShowInputRequested(flags, configChange); + } + + @Override public boolean onEvaluateInputViewShown() { if (mIsExecutingStartShowingInputView) { return true; @@ -1207,8 +1266,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void updateFullscreenMode() { + super.updateFullscreenMode(); + mInputLogic.onUpdateFullscreenMode(isFullscreenMode()); + updateSoftInputWindowLayoutParameters(); + } + + private void updateSoftInputWindowLayoutParameters() { // Override layout parameters to expand {@link SoftInputWindow} to the entire screen. - // See {@link InputMethodService#setinputView(View) and + // See {@link InputMethodService#setinputView(View)} and // {@link SoftInputWindow#updateWidthHeight(WindowManager.LayoutParams)}. final Window window = getWindow().getWindow(); ViewLayoutUtils.updateLayoutHeightOf(window, LayoutParams.MATCH_PARENT); @@ -1227,8 +1292,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen ViewLayoutUtils.updateLayoutGravityOf(inputArea, Gravity.BOTTOM); ViewLayoutUtils.updateLayoutHeightOf(mInputView, layoutHeight); } - super.updateFullscreenMode(); - mInputLogic.onUpdateFullscreenMode(isFullscreenMode()); } private int getCurrentAutoCapsState() { @@ -1239,10 +1302,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mInputLogic.getCurrentRecapitalizeState(); } - public Locale getCurrentSubtypeLocale() { - return mSubtypeSwitcher.getCurrentSubtypeLocale(); - } - /** * @param codePoints code points to get coordinates for. * @return x,y coordinates for this keyboard, as a flattened array. @@ -1264,13 +1323,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Probably never supposed to happen, but just in case. return; } - final String wordToEdit; - if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) { - wordToEdit = word.toLowerCase(getCurrentSubtypeLocale()); - } else { - wordToEdit = word; - } - mDictionaryFacilitator.addWordToUserDictionary(this /* context */, wordToEdit); + mDictionaryFacilitator.addWordToUserDictionary(this /* context */, word); mInputLogic.onAddWordToUserDictionary(); } @@ -1328,48 +1381,57 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSubtypeState.switchSubtype(token, mRichImm); } + // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for + // alphabetic shift and shift while in symbol layout and get rid of this method. + private int getCodePointForKeyboard(final int codePoint) { + if (Constants.CODE_SHIFT == codePoint) { + final Keyboard currentKeyboard = mKeyboardSwitcher.getKeyboard(); + if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { + return codePoint; + } else { + return Constants.CODE_SYMBOL_SHIFT; + } + } else { + return codePoint; + } + } + // Implementation of {@link KeyboardActionListener}. @Override public void onCodeInput(final int codePoint, final int x, final int y, final boolean isKeyRepeat) { + // TODO: this processing does not belong inside LatinIME, the caller should be doing this. final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); // x and y include some padding, but everything down the line (especially native // code) needs the coordinates in the keyboard frame. // TODO: We should reconsider which coordinate system should be used to represent // keyboard event. Also we should pull this up -- LatinIME has no business doing - // this transformation, it should be done already before calling onCodeInput. + // this transformation, it should be done already before calling onEvent. final int keyX = mainKeyboardView.getKeyX(x); final int keyY = mainKeyboardView.getKeyY(y); - final int codeToSend; - if (Constants.CODE_SHIFT == codePoint) { - // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for - // alphabetic shift and shift while in symbol layout. - final Keyboard currentKeyboard = mKeyboardSwitcher.getKeyboard(); - if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) { - codeToSend = codePoint; - } else { - codeToSend = Constants.CODE_SYMBOL_SHIFT; - } - } else { - codeToSend = codePoint; - } - if (Constants.CODE_SHORTCUT == codePoint) { + final Event event = createSoftwareKeypressEvent(getCodePointForKeyboard(codePoint), + keyX, keyY, isKeyRepeat); + onEvent(event); + } + + // This method is public for testability of LatinIME, but also in the future it should + // completely replace #onCodeInput. + public void onEvent(final Event event) { + if (Constants.CODE_SHORTCUT == event.mKeyCode) { mSubtypeSwitcher.switchToShortcutIME(this); - // Still call the *#onCodeInput methods for readability. } - final Event event = createSoftwareKeypressEvent(codeToSend, keyX, keyY, isKeyRepeat); final InputTransaction completeInputTransaction = mInputLogic.onCodeInput(mSettings.getCurrent(), event, mKeyboardSwitcher.getKeyboardShiftMode(), mKeyboardSwitcher.getCurrentKeyboardScriptId(), mHandler); updateStateAfterInputTransaction(completeInputTransaction); - mKeyboardSwitcher.onCodeInput(codePoint, getCurrentAutoCapsState(), - getCurrentRecapitalizeState()); + mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } // A helper method to split the code point and the key code. Ultimately, they should not be // squashed into the same variable, and this method should be removed. - private static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int keyX, + // public for testing, as we don't want to copy the same logic into test code + public static Event createSoftwareKeypressEvent(final int keyCodeOrCodePoint, final int keyX, final int keyY, final boolean isKeyRepeat) { final int keyCode; final int codePoint; @@ -1387,18 +1449,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onTextInput(final String rawText) { // TODO: have the keyboard pass the correct key code when we need it. - final Event event = Event.createSoftwareTextEvent(rawText, Event.NOT_A_KEY_CODE); + final Event event = Event.createSoftwareTextEvent(rawText, Constants.CODE_OUTPUT_TEXT); final InputTransaction completeInputTransaction = mInputLogic.onTextInput(mSettings.getCurrent(), event, mKeyboardSwitcher.getKeyboardShiftMode(), mHandler); updateStateAfterInputTransaction(completeInputTransaction); - mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT, getCurrentAutoCapsState(), - getCurrentRecapitalizeState()); + mKeyboardSwitcher.onEvent(event, getCurrentAutoCapsState(), getCurrentRecapitalizeState()); } @Override public void onStartBatchInput() { mInputLogic.onStartBatchInput(mSettings.getCurrent(), mKeyboardSwitcher, mHandler); + mGestureConsumer.onGestureStarted( + Arrays.asList(mSubtypeSwitcher.getCurrentSubtypeLocales()), + mKeyboardSwitcher.getKeyboard()); } @Override @@ -1409,11 +1473,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onEndBatchInput(final InputPointers batchPointers) { mInputLogic.onEndBatchInput(batchPointers); + mGestureConsumer.onGestureCompleted(batchPointers); } @Override public void onCancelBatchInput() { mInputLogic.onCancelBatchInput(mHandler); + mGestureConsumer.onGestureCanceled(); + } + + /** + * To be called after the InputLogic has gotten a chance to act on the on-device decoding + * for the full gesture, possibly updating the TextView to reflect the first decoding. + * <p> + * This method must be run on the UI Thread. + * @param suggestedWords On-device decoding for the full gesture. + */ + public void onTailBatchInputResultShown(final SuggestedWords suggestedWords) { + mGestureConsumer.onImeSuggestionsProcessed(suggestedWords, + mInputLogic.getComposingStart(), mInputLogic.getComposingLength()); } // This method must run on the UI Thread. @@ -1461,7 +1539,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void setSuggestedWords(final SuggestedWords suggestedWords) { final SettingsValues currentSettingsValues = mSettings.getCurrent(); - mInputLogic.setSuggestedWords(suggestedWords, currentSettingsValues, mHandler); + mInputLogic.setSuggestedWords(suggestedWords); // TODO: Modify this when we support suggestions with hard keyboard if (!hasSuggestionStripView()) { return; @@ -1489,7 +1567,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final boolean isEmptyApplicationSpecifiedCompletions = currentSettingsValues.isApplicationSpecifiedCompletionsOn() && suggestedWords.isEmpty(); - final boolean noSuggestionsFromDictionaries = (SuggestedWords.EMPTY == suggestedWords) + final boolean noSuggestionsFromDictionaries = suggestedWords.isEmpty() || suggestedWords.isPunctuationSuggestions() || isEmptyApplicationSpecifiedCompletions; final boolean isBeginningOfSentencePrediction = (suggestedWords.mInputStyle @@ -1507,7 +1585,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // We should clear the contextual strip if there is no suggestion from dictionaries. || noSuggestionsFromDictionaries) { mSuggestionStripView.setSuggestions(suggestedWords, - SubtypeLocaleUtils.isRtlLanguage(mSubtypeSwitcher.getCurrentSubtype())); + mSubtypeSwitcher.getCurrentSubtype().isRtlSubtype()); } } @@ -1516,7 +1594,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final OnGetSuggestedWordsCallback callback) { final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); if (keyboard == null) { - callback.onGetSuggestedWords(SuggestedWords.EMPTY); + callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance()); return; } mInputLogic.getSuggestedWords(mSettings.getCurrent(), keyboard.getProximityInfo(), @@ -1524,10 +1602,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public void showSuggestionStrip(final SuggestedWords sourceSuggestedWords) { - final SuggestedWords suggestedWords = - sourceSuggestedWords.isEmpty() ? SuggestedWords.EMPTY : sourceSuggestedWords; - if (SuggestedWords.EMPTY == suggestedWords) { + public void showSuggestionStrip(final SuggestedWords suggestedWords) { + if (suggestedWords.isEmpty()) { setNeutralSuggestionStrip(); } else { setSuggestedWords(suggestedWords); @@ -1535,7 +1611,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Cache the auto-correction in accessibility code so we can speak it if the user // touches a key that will insert it. AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, - sourceSuggestedWords.mTypedWord); + suggestedWords.mTypedWord); } // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} @@ -1551,11 +1627,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } @Override - public void showAddToDictionaryHint(final String word) { + public void suggestAddingToDictionary(final String word, final boolean isFromSuggestionStrip) { if (!hasSuggestionStripView()) { return; } - mSuggestionStripView.showAddToDictionaryHint(word); + final String wordToShow; + if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) { + wordToShow = word.toLowerCase(mDictionaryFacilitator.getMostProbableLocale()); + } else { + wordToShow = word; + } + mSuggestionStripView.showAddToDictionaryHint(wordToShow, + isFromSuggestionStrip /* shouldShowWordToSave */); } // This will show either an empty suggestion strip (if prediction is enabled) or @@ -1564,7 +1647,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void setNeutralSuggestionStrip() { final SettingsValues currentSettings = mSettings.getCurrent(); final SuggestedWords neutralSuggestions = currentSettings.mBigramPredictionEnabled - ? SuggestedWords.EMPTY : currentSettings.mSpacingAndPunctuations.mSuggestPuncList; + ? SuggestedWords.getEmptyInstance() + : currentSettings.mSpacingAndPunctuations.mSuggestPuncList; setSuggestedWords(neutralSuggestions); } @@ -1816,7 +1900,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @UsedForTesting /* package for test */ void replaceDictionariesForTest(final Locale locale) { final SettingsValues settingsValues = mSettings.getCurrent(); - mDictionaryFacilitator.resetDictionaries(this, locale, + mDictionaryFacilitator.resetDictionaries(this, new Locale[] { locale }, settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts, false /* forceReloadMainDictionary */, this /* listener */); } @@ -1835,8 +1919,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } public void dumpDictionaryForDebug(final String dictName) { - if (mDictionaryFacilitator.getLocale() == null) { - resetSuggest(); + if (!mDictionaryFacilitator.isActive()) { + resetDictionaryFacilitatorIfNecessary(); } mDictionaryFacilitator.dumpDictionaryForDebug(dictName); } diff --git a/java/src/com/android/inputmethod/latin/PrevWordsInfo.java b/java/src/com/android/inputmethod/latin/NgramContext.java index db877ab7a..a02531cc4 100644 --- a/java/src/com/android/inputmethod/latin/PrevWordsInfo.java +++ b/java/src/com/android/inputmethod/latin/NgramContext.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin; import android.text.TextUtils; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.utils.StringUtils; import java.util.Arrays; @@ -26,11 +27,11 @@ import java.util.Arrays; * Class to represent information of previous words. This class is used to add n-gram entries * into binary dictionaries, to get predictions, and to get suggestions. */ -public class PrevWordsInfo { - public static final PrevWordsInfo EMPTY_PREV_WORDS_INFO = - new PrevWordsInfo(WordInfo.EMPTY_WORD_INFO); - public static final PrevWordsInfo BEGINNING_OF_SENTENCE = - new PrevWordsInfo(WordInfo.BEGINNING_OF_SENTENCE); +public class NgramContext { + public static final NgramContext EMPTY_PREV_WORDS_INFO = + new NgramContext(WordInfo.EMPTY_WORD_INFO); + public static final NgramContext BEGINNING_OF_SENTENCE = + new NgramContext(WordInfo.BEGINNING_OF_SENTENCE); /** * Word information used to represent previous words information. @@ -86,38 +87,66 @@ public class PrevWordsInfo { // For simplicity of implementation, elements may also be EMPTY_WORD_INFO transiently after the // WordComposer was reset and before starting a new composing word, but we should never be // calling getSuggetions* in this situation. - public WordInfo[] mPrevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + private final WordInfo[] mPrevWordsInfo; + private final int mPrevWordsCount; // Construct from the previous word information. - public PrevWordsInfo(final WordInfo prevWordInfo) { - mPrevWordsInfo[0] = prevWordInfo; + public NgramContext(final WordInfo... prevWordsInfo) { + mPrevWordsInfo = prevWordsInfo; + mPrevWordsCount = prevWordsInfo.length; } - // Construct from WordInfo array. n-th element represents (n+1)-th previous word's information. - public PrevWordsInfo(final WordInfo[] prevWordsInfo) { - for (int i = 0; i < Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM; i++) { - mPrevWordsInfo[i] = - (prevWordsInfo.length > i) ? prevWordsInfo[i] : WordInfo.EMPTY_WORD_INFO; + // Construct from WordInfo array and size. The caller shouldn't change prevWordsInfo after + // calling this method. + private NgramContext(final NgramContext ngramContext, final int prevWordsCount) { + if (ngramContext.mPrevWordsCount < prevWordsCount) { + throw new IndexOutOfBoundsException("ngramContext.mPrevWordsCount (" + + ngramContext.mPrevWordsCount + ") is smaller than prevWordsCount (" + + prevWordsCount + ")"); } + mPrevWordsInfo = ngramContext.mPrevWordsInfo; + mPrevWordsCount = prevWordsCount; } // Create next prevWordsInfo using current prevWordsInfo. - public PrevWordsInfo getNextPrevWordsInfo(final WordInfo wordInfo) { - final WordInfo[] prevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + public NgramContext getNextNgramContext(final WordInfo wordInfo) { + final int nextPrevWordCount = Math.min(Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM, + mPrevWordsCount + 1); + final WordInfo[] prevWordsInfo = new WordInfo[nextPrevWordCount]; prevWordsInfo[0] = wordInfo; - for (int i = 1; i < prevWordsInfo.length; i++) { - prevWordsInfo[i] = mPrevWordsInfo[i - 1]; - } - return new PrevWordsInfo(prevWordsInfo); + System.arraycopy(mPrevWordsInfo, 0, prevWordsInfo, 1, nextPrevWordCount - 1); + return new NgramContext(prevWordsInfo); } public boolean isValid() { - return mPrevWordsInfo[0].isValid(); + return mPrevWordsCount > 0 && mPrevWordsInfo[0].isValid(); + } + + public boolean isBeginningOfSentenceContext() { + return mPrevWordsCount > 0 && mPrevWordsInfo[0].mIsBeginningOfSentence; + } + + // n is 1-indexed. + // TODO: Remove + public CharSequence getNthPrevWord(final int n) { + if (n <= 0 || n > mPrevWordsCount) { + return null; + } + return mPrevWordsInfo[n - 1].mWord; + } + + // n is 1-indexed. + @UsedForTesting + public boolean isNthPrevWordBeginningOfSontence(final int n) { + if (n <= 0 || n > mPrevWordsCount) { + return false; + } + return mPrevWordsInfo[n - 1].mIsBeginningOfSentence; } public void outputToArray(final int[][] codePointArrays, final boolean[] isBeginningOfSentenceArray) { - for (int i = 0; i < mPrevWordsInfo.length; i++) { + for (int i = 0; i < mPrevWordsCount; i++) { final WordInfo wordInfo = mPrevWordsInfo[i]; if (wordInfo == null || !wordInfo.isValid()) { codePointArrays[i] = new int[0]; @@ -129,28 +158,65 @@ public class PrevWordsInfo { } } + public int getPrevWordCount() { + return mPrevWordsCount; + } + @Override public int hashCode() { - return Arrays.hashCode(mPrevWordsInfo); + int hashValue = 0; + for (final WordInfo wordInfo : mPrevWordsInfo) { + if (wordInfo == null || !WordInfo.EMPTY_WORD_INFO.equals(wordInfo)) { + break; + } + hashValue ^= wordInfo.hashCode(); + } + return hashValue; } @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof PrevWordsInfo)) return false; - final PrevWordsInfo prevWordsInfo = (PrevWordsInfo)o; - return Arrays.equals(mPrevWordsInfo, prevWordsInfo.mPrevWordsInfo); + if (!(o instanceof NgramContext)) return false; + final NgramContext prevWordsInfo = (NgramContext)o; + + final int minLength = Math.min(mPrevWordsCount, prevWordsInfo.mPrevWordsCount); + for (int i = 0; i < minLength; i++) { + if (!mPrevWordsInfo[i].equals(prevWordsInfo.mPrevWordsInfo[i])) { + return false; + } + } + final WordInfo[] longerWordsInfo; + final int longerWordsInfoCount; + if (mPrevWordsCount > prevWordsInfo.mPrevWordsCount) { + longerWordsInfo = mPrevWordsInfo; + longerWordsInfoCount = mPrevWordsCount; + } else { + longerWordsInfo = prevWordsInfo.mPrevWordsInfo; + longerWordsInfoCount = prevWordsInfo.mPrevWordsCount; + } + for (int i = minLength; i < longerWordsInfoCount; i++) { + if (longerWordsInfo[i] != null + && !WordInfo.EMPTY_WORD_INFO.equals(longerWordsInfo[i])) { + return false; + } + } + return true; } @Override public String toString() { final StringBuffer builder = new StringBuffer(); - for (int i = 0; i < mPrevWordsInfo.length; i++) { + for (int i = 0; i < mPrevWordsCount; i++) { final WordInfo wordInfo = mPrevWordsInfo[i]; builder.append("PrevWord["); builder.append(i); builder.append("]: "); - if (wordInfo == null || !wordInfo.isValid()) { + if (wordInfo == null) { + builder.append("null. "); + continue; + } + if (!wordInfo.isValid()) { builder.append("Empty. "); continue; } diff --git a/java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java new file mode 100644 index 000000000..396d062f8 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/PersonalizationHelperForDictionaryFacilitator.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; + +import android.content.Context; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.latin.ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback; +import com.android.inputmethod.latin.personalization.PersonalizationDataChunk; +import com.android.inputmethod.latin.personalization.PersonalizationDictionary; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; +import com.android.inputmethod.latin.utils.DistracterFilter; +import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary; +import com.android.inputmethod.latin.utils.LanguageModelParam; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; + +/** + * Class for managing and updating personalization dictionaries. + */ +public class PersonalizationHelperForDictionaryFacilitator { + private final Context mContext; + private final DistracterFilter mDistracterFilter; + private final HashMap<String, HashSet<Locale>> mLangToLocalesMap = new HashMap<>(); + private final HashMap<Locale, ExpandableBinaryDictionary> mPersonalizationDictsToUpdate = + new HashMap<>(); + private boolean mIsMonolingualUser = false; + + PersonalizationHelperForDictionaryFacilitator(final Context context, + final DistracterFilter distracterFilter) { + mContext = context; + mDistracterFilter = distracterFilter; + } + + public void close() { + mLangToLocalesMap.clear(); + for (final ExpandableBinaryDictionary dict : mPersonalizationDictsToUpdate.values()) { + dict.close(); + } + mPersonalizationDictsToUpdate.clear(); + } + + public void clearDictionariesToUpdate() { + for (final ExpandableBinaryDictionary dict : mPersonalizationDictsToUpdate.values()) { + dict.clear(); + } + mPersonalizationDictsToUpdate.clear(); + } + + public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { + for (final InputMethodSubtype subtype : enabledSubtypes) { + final Locale locale = SubtypeLocaleUtils.getSubtypeLocale(subtype); + final String language = locale.getLanguage(); + final HashSet<Locale> locales = mLangToLocalesMap.get(language); + if (locales != null) { + locales.add(locale); + } else { + final HashSet<Locale> localeSet = new HashSet<>(); + localeSet.add(locale); + mLangToLocalesMap.put(language, localeSet); + } + } + } + + public void setIsMonolingualUser(final boolean isMonolingualUser) { + mIsMonolingualUser = isMonolingualUser; + } + + /** + * Flush personalization dictionaries to dictionary files. Close dictionaries after writing + * files except the dictionaries that is used for generating suggestions. + * + * @param personalizationDictsUsedForSuggestion the personalization dictionaries used for + * generating suggestions that won't be closed. + */ + public void flushPersonalizationDictionariesToUpdate( + final HashSet<ExpandableBinaryDictionary> personalizationDictsUsedForSuggestion) { + for (final ExpandableBinaryDictionary personalizationDict : + mPersonalizationDictsToUpdate.values()) { + personalizationDict.asyncFlushBinaryDictionary(); + if (!personalizationDictsUsedForSuggestion.contains(personalizationDict)) { + // Close if the dictionary is not being used for suggestion. + personalizationDict.close(); + } + } + mDistracterFilter.close(); + mPersonalizationDictsToUpdate.clear(); + } + + private ExpandableBinaryDictionary getPersonalizationDictToUpdate(final Context context, + final Locale locale) { + ExpandableBinaryDictionary personalizationDict = mPersonalizationDictsToUpdate.get(locale); + if (personalizationDict != null) { + return personalizationDict; + } + personalizationDict = PersonalizationDictionary.getDictionary(context, locale, + null /* dictFile */, "" /* dictNamePrefix */); + mPersonalizationDictsToUpdate.put(locale, personalizationDict); + return personalizationDict; + } + + private void addEntriesToPersonalizationDictionariesForLocale(final Locale locale, + final PersonalizationDataChunk personalizationDataChunk, + final SpacingAndPunctuations spacingAndPunctuations, + final AddMultipleDictionaryEntriesCallback callback) { + final ExpandableBinaryDictionary personalizationDict = + getPersonalizationDictToUpdate(mContext, locale); + if (personalizationDict == null) { + if (callback != null) { + callback.onFinished(); + } + return; + } + final ArrayList<LanguageModelParam> languageModelParams = + LanguageModelParam.createLanguageModelParamsFrom( + personalizationDataChunk.mTokens, + personalizationDataChunk.mTimestampInSeconds, spacingAndPunctuations, + locale, new DistracterFilterCheckingIsInDictionary( + mDistracterFilter, personalizationDict)); + if (languageModelParams == null || languageModelParams.isEmpty()) { + if (callback != null) { + callback.onFinished(); + } + return; + } + personalizationDict.addMultipleDictionaryEntriesDynamically(languageModelParams, callback); + } + + public void addEntriesToPersonalizationDictionariesToUpdate(final Locale defaultLocale, + final PersonalizationDataChunk personalizationDataChunk, + final SpacingAndPunctuations spacingAndPunctuations, + final AddMultipleDictionaryEntriesCallback callback) { + final String language = personalizationDataChunk.mDetectedLanguage; + final HashSet<Locale> locales; + if (mIsMonolingualUser && PersonalizationDataChunk.LANGUAGE_UNKNOWN.equals(language) + && mLangToLocalesMap.size() == 1) { + locales = mLangToLocalesMap.get(defaultLocale.getLanguage()); + } else { + locales = mLangToLocalesMap.get(language); + } + if (locales == null || locales.isEmpty()) { + if (callback != null) { + callback.onFinished(); + } + return; + } + final AtomicInteger remainingTaskCount = new AtomicInteger(locales.size()); + final AddMultipleDictionaryEntriesCallback callbackForLocales = + new AddMultipleDictionaryEntriesCallback() { + @Override + public void onFinished() { + if (remainingTaskCount.decrementAndGet() == 0) { + // Update tasks for all locales have been finished. + if (callback != null) { + callback.onFinished(); + } + } + } + }; + for (final Locale locale : locales) { + addEntriesToPersonalizationDictionariesForLocale(locale, personalizationDataChunk, + spacingAndPunctuations, callbackForLocales); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java index 5d4fc5861..bc8bd831c 100644 --- a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java @@ -40,7 +40,7 @@ public final class ReadOnlyBinaryDictionary extends Dictionary { public ReadOnlyBinaryDictionary(final String filename, final long offset, final long length, final boolean useFullEditDistance, final Locale locale, final String dictType) { - super(dictType); + super(dictType, locale); mBinaryDictionary = new BinaryDictionary(filename, offset, length, useFullEditDistance, locale, dictType, false /* isUpdatable */); } @@ -51,13 +51,15 @@ public final class ReadOnlyBinaryDictionary extends Dictionary { @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final NgramContext ngramContext, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, - final int sessionId, final float[] inOutLanguageWeight) { + final int sessionId, final float weightForLocale, + final float[] inOutWeightOfLangModelVsSpatialModel) { if (mLock.readLock().tryLock()) { try { - return mBinaryDictionary.getSuggestions(composer, prevWordsInfo, proximityInfo, - settingsValuesForSuggestion, sessionId, inOutLanguageWeight); + return mBinaryDictionary.getSuggestions(composer, ngramContext, proximityInfo, + settingsValuesForSuggestion, sessionId, weightForLocale, + inOutWeightOfLangModelVsSpatialModel); } finally { mLock.readLock().unlock(); } diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java index 744b0321a..a3f7bb4d6 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -19,6 +19,7 @@ package com.android.inputmethod.latin; import android.graphics.Color; import android.inputmethodservice.InputMethodService; import android.os.Build; +import android.os.Bundle; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -33,10 +34,11 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import com.android.inputmethod.compat.InputConnectionCompatUtils; +import com.android.inputmethod.latin.inputlogic.PrivateCommandPerformer; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import com.android.inputmethod.latin.utils.CapsModeUtils; import com.android.inputmethod.latin.utils.DebugLogUtils; -import com.android.inputmethod.latin.utils.PrevWordsInfoUtils; +import com.android.inputmethod.latin.utils.NgramContextUtils; import com.android.inputmethod.latin.utils.ScriptUtils; import com.android.inputmethod.latin.utils.SpannableStringUtils; import com.android.inputmethod.latin.utils.StringUtils; @@ -52,7 +54,7 @@ import java.util.Arrays; * all the time to find out what text is in the buffer, when we need it to determine caps mode * for example. */ -public final class RichInputConnection { +public final class RichInputConnection implements PrivateCommandPerformer { private static final String TAG = RichInputConnection.class.getSimpleName(); private static final boolean DBG = false; private static final boolean DEBUG_PREVIOUS_TEXT = false; @@ -593,11 +595,11 @@ public final class RichInputConnection { } @SuppressWarnings("unused") - public PrevWordsInfo getPrevWordsInfoFromNthPreviousWord( + public NgramContext getNgramContextFromNthPreviousWord( final SpacingAndPunctuations spacingAndPunctuations, final int n) { mIC = mParent.getCurrentInputConnection(); if (null == mIC) { - return PrevWordsInfo.EMPTY_PREV_WORDS_INFO; + return NgramContext.EMPTY_PREV_WORDS_INFO; } final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); if (DEBUG_PREVIOUS_TEXT && null != prev) { @@ -618,7 +620,7 @@ public final class RichInputConnection { } } } - return PrevWordsInfoUtils.getPrevWordsInfoFromNthPreviousWord( + return NgramContextUtils.getNgramContextFromNthPreviousWord( prev, spacingAndPunctuations, n); } @@ -740,17 +742,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 @@ -847,17 +851,32 @@ public final class RichInputConnection { /** * Try to get the text from the editor to expose lies the framework may have been - * telling us. Concretely, when the device rotates, the frameworks tells us about where the - * cursor used to be initially in the editor at the time it first received the focus; this + * telling us. Concretely, when the device rotates and when the keyboard reopens in the same + * text field after having been closed with the back key, the frameworks tells us about where + * the cursor used to be initially in the editor at the time it first received the focus; this * may be completely different from the place it is upon rotation. Since we don't have any * means to get the real value, try at least to ask the text view for some characters and * detect the most damaging cases: when the cursor position is declared to be much smaller * than it really is. */ public void tryFixLyingCursorPosition() { + mIC = mParent.getCurrentInputConnection(); final CharSequence textBeforeCursor = getTextBeforeCursor( Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); - if (null == textBeforeCursor) { + final CharSequence selectedText = null == mIC ? null : mIC.getSelectedText(0 /* flags */); + if (null == textBeforeCursor || + (!TextUtils.isEmpty(selectedText) && mExpectedSelEnd == mExpectedSelStart)) { + // If textBeforeCursor is null, we have no idea what kind of text field we have or if + // thinking about the "cursor position" actually makes any sense. In this case we + // remember a meaningless cursor position. Contrast this with an empty string, which is + // valid and should mean the cursor is at the start of the text. + // Also, if we expect we don't have a selection but we DO have non-empty selected text, + // then the framework lied to us about the cursor position. In this case, we should just + // revert to the most basic behavior possible for the next action (backspace in + // particular comes to mind), so we remember a meaningless cursor position which should + // result in degraded behavior from the next input. + // Interestingly, in either case, chances are any action the user takes next will result + // in a call to onUpdateSelection, which should set things right. mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION; } else { final int textLength = textBeforeCursor.length(); @@ -880,6 +899,15 @@ public final class RichInputConnection { } } + @Override + public boolean performPrivateCommand(final String action, final Bundle data) { + mIC = mParent.getCurrentInputConnection(); + if (mIC == null) { + return false; + } + return mIC.performPrivateCommand(action, data); + } + public int getExpectedSelectionStart() { return mExpectedSelStart; } diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java index 7cf4eff92..b0c0725bb 100644 --- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java +++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java @@ -20,6 +20,7 @@ import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Resources; import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; @@ -29,6 +30,7 @@ import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; +import com.android.inputmethod.latin.settings.AdditionalFeaturesSettingUtils; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; @@ -40,7 +42,8 @@ import java.util.List; /** * Enrichment class for InputMethodManager to simplify interaction and add functionality. */ -public final class RichInputMethodManager { +// non final for easy mocking. +public class RichInputMethodManager { private static final String TAG = RichInputMethodManager.class.getSimpleName(); private RichInputMethodManager() { @@ -49,6 +52,7 @@ public final class RichInputMethodManager { private static final RichInputMethodManager sInstance = new RichInputMethodManager(); + private Resources mResources; private InputMethodManagerCompatWrapper mImmWrapper; private InputMethodInfoCache mInputMethodInfoCache; final HashMap<InputMethodInfo, List<InputMethodSubtype>> @@ -82,6 +86,7 @@ public final class RichInputMethodManager { return; } mImmWrapper = new InputMethodManagerCompatWrapper(context); + mResources = context.getResources(); mInputMethodInfoCache = new InputMethodInfoCache( mImmWrapper.mImm, context.getPackageName()); @@ -297,10 +302,14 @@ public final class RichInputMethodManager { return INDEX_NOT_FOUND; } - public InputMethodSubtype getCurrentInputMethodSubtype( - final InputMethodSubtype defaultSubtype) { - final InputMethodSubtype currentSubtype = mImmWrapper.mImm.getCurrentInputMethodSubtype(); - return (currentSubtype != null) ? currentSubtype : defaultSubtype; + public InputMethodSubtype getCurrentRawSubtype() { + return mImmWrapper.mImm.getCurrentInputMethodSubtype(); + } + + public RichInputMethodSubtype createCurrentRichInputMethodSubtype( + final InputMethodSubtype rawSubtype) { + return AdditionalFeaturesSettingUtils.createRichInputMethodSubtype(this, rawSubtype, + mResources); } public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) { diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java new file mode 100644 index 000000000..8d055531c --- /dev/null +++ b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.latin.utils.LocaleUtils; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; + +import java.util.Arrays; +import java.util.Locale; + +/** + * Enrichment class for InputMethodSubtype to enable concurrent multi-lingual input. + * + * Right now, this returns the extra value of its primary subtype. + */ +public final class RichInputMethodSubtype { + private final InputMethodSubtype mSubtype; + private final Locale[] mLocales; + + public RichInputMethodSubtype(final InputMethodSubtype subtype, final Locale... locales) { + mSubtype = subtype; + mLocales = new Locale[locales.length+1]; + mLocales[0] = LocaleUtils.constructLocaleFromString(mSubtype.getLocale()); + System.arraycopy(locales, 0, mLocales, 1, locales.length); + } + + // Extra values are determined by the primary subtype. This is probably right, but + // we may have to revisit this later. + public String getExtraValueOf(final String key) { + return mSubtype.getExtraValueOf(key); + } + + // The mode is also determined by the primary subtype. + public String getMode() { + return mSubtype.getMode(); + } + + public boolean isNoLanguage() { + if (mLocales.length > 1) { + return false; + } + return SubtypeLocaleUtils.NO_LANGUAGE.equals(mSubtype.getLocale()); + } + + public String getNameForLogging() { + return toString(); + } + + // InputMethodSubtype's display name for spacebar text in its locale. + // isAdditionalSubtype (T=true, F=false) + // locale layout | Middle Full + // ------ ------- - --------- ---------------------- + // en_US qwerty F English English (US) exception + // en_GB qwerty F English English (UK) exception + // es_US spanish F Español Español (EE.UU.) exception + // fr azerty F Français Français + // fr_CA qwerty F Français Français (Canada) + // fr_CH swiss F Français Français (Suisse) + // de qwertz F Deutsch Deutsch + // de_CH swiss T Deutsch Deutsch (Schweiz) + // zz qwerty F QWERTY QWERTY + // fr qwertz T Français Français + // de qwerty T Deutsch Deutsch + // en_US azerty T English English (US) + // zz azerty T AZERTY AZERTY + // Get the RichInputMethodSubtype's full display name in its locale. + public String getFullDisplayName() { + if (isNoLanguage()) { + return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype); + } + return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(mSubtype.getLocale()); + } + + // Get the RichInputMethodSubtype's middle display name in its locale. + public String getMiddleDisplayName() { + if (isNoLanguage()) { + return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype); + } + return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(mSubtype.getLocale()); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof RichInputMethodSubtype)) { + return false; + } + final RichInputMethodSubtype other = (RichInputMethodSubtype)o; + return mSubtype.equals(other.mSubtype) && Arrays.equals(mLocales, other.mLocales); + } + + @Override + public int hashCode() { + return mSubtype.hashCode() + Arrays.hashCode(mLocales); + } + + @Override + public String toString() { + return "Multi-lingual subtype: " + mSubtype.toString() + ", " + Arrays.toString(mLocales); + } + + public Locale[] getLocales() { + return mLocales; + } + + public boolean isRtlSubtype() { + // The subtype is considered RTL if the language of the main subtype is RTL. + return SubtypeLocaleUtils.isRtlLanguage(mLocales[0]); + } + + // TODO: remove this method + public InputMethodSubtype getRawSubtype() { return mSubtype; } +} diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java index a725e1611..13f79b49f 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -36,7 +36,6 @@ import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.internal.LanguageOnSpacebarHelper; import com.android.inputmethod.latin.define.DebugFlags; -import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import java.util.HashSet; @@ -58,8 +57,9 @@ public final class SubtypeSwitcher { new LanguageOnSpacebarHelper(); private InputMethodInfo mShortcutInputMethodInfo; private InputMethodSubtype mShortcutSubtype; - private InputMethodSubtype mNoLanguageSubtype; - private InputMethodSubtype mEmojiSubtype; + private RichInputMethodSubtype mCurrentRichInputMethodSubtype; + private RichInputMethodSubtype mNoLanguageSubtype; + private RichInputMethodSubtype mEmojiSubtype; private boolean mIsNetworkConnected; private static final String KEYBOARD_MODE = "keyboard"; @@ -70,26 +70,26 @@ public final class SubtypeSwitcher { + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE; - private static final InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE = - InputMethodSubtypeCompatUtils.newInputMethodSubtype( + private static final RichInputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE = + new RichInputMethodSubtype(InputMethodSubtypeCompatUtils.newInputMethodSubtype( R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark, SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE, EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE, false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */, - SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE); + SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE)); // Caveat: We probably should remove this when we add an Emoji subtype in {@link R.xml.method}. // Dummy Emoji subtype. See {@link R.xml.method}. private static final int SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE = 0xd78b2ed0; private static final String EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE = "KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE; - private static final InputMethodSubtype DUMMY_EMOJI_SUBTYPE = + private static final RichInputMethodSubtype DUMMY_EMOJI_SUBTYPE = new RichInputMethodSubtype( InputMethodSubtypeCompatUtils.newInputMethodSubtype( R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark, SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE, EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE, false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */, - SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE); + SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE)); public static SubtypeSwitcher getInstance() { return sInstance; @@ -117,7 +117,7 @@ public final class SubtypeSwitcher { final NetworkInfo info = connectivityManager.getActiveNetworkInfo(); mIsNetworkConnected = (info != null && info.isConnected()); - onSubtypeChanged(getCurrentSubtype()); + onSubtypeChanged(mRichImm.getCurrentRawSubtype()); updateParametersOnStartInputView(); } @@ -166,20 +166,27 @@ public final class SubtypeSwitcher { // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function. public void onSubtypeChanged(final InputMethodSubtype newSubtype) { + final RichInputMethodSubtype richSubtype = + mRichImm.createCurrentRichInputMethodSubtype(newSubtype); if (DBG) { - Log.w(TAG, "onSubtypeChanged: " - + SubtypeLocaleUtils.getSubtypeNameForLogging(newSubtype)); + Log.w(TAG, "onSubtypeChanged: " + richSubtype.getNameForLogging()); + } + mCurrentRichInputMethodSubtype = richSubtype; + final Locale[] newLocales = richSubtype.getLocales(); + if (newLocales.length > 1) { + // In multi-locales mode, the system language is never the same as the input language + // because there is no single input language. + mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(false); + } else { + final Locale newLocale = newLocales[0]; + final Locale systemLocale = mResources.getConfiguration().locale; + final boolean sameLocale = systemLocale.equals(newLocale); + final boolean sameLanguage = systemLocale.getLanguage().equals(newLocale.getLanguage()); + final boolean implicitlyEnabled = mRichImm + .checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype); + mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage( + sameLocale || (sameLanguage && implicitlyEnabled)); } - - final Locale newLocale = SubtypeLocaleUtils.getSubtypeLocale(newSubtype); - final Locale systemLocale = mResources.getConfiguration().locale; - final boolean sameLocale = systemLocale.equals(newLocale); - final boolean sameLanguage = systemLocale.getLanguage().equals(newLocale.getLanguage()); - final boolean implicitlyEnabled = - mRichImm.checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype); - mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage( - sameLocale || (sameLanguage && implicitlyEnabled)); - updateShortcutIME(); } @@ -250,7 +257,7 @@ public final class SubtypeSwitcher { // Subtype Switching functions // ////////////////////////////////// - public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) { + public int getLanguageOnSpacebarFormatType(final RichInputMethodSubtype subtype) { return mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(subtype); } @@ -279,30 +286,31 @@ public final class SubtypeSwitcher { return true; } - private static InputMethodSubtype sForcedSubtypeForTesting = null; + private static RichInputMethodSubtype sForcedSubtypeForTesting = null; @UsedForTesting void forceSubtype(final InputMethodSubtype subtype) { - sForcedSubtypeForTesting = subtype; + sForcedSubtypeForTesting = new RichInputMethodSubtype(subtype); } - public Locale getCurrentSubtypeLocale() { + public Locale[] getCurrentSubtypeLocales() { if (null != sForcedSubtypeForTesting) { - return LocaleUtils.constructLocaleFromString(sForcedSubtypeForTesting.getLocale()); + return sForcedSubtypeForTesting.getLocales(); } - return SubtypeLocaleUtils.getSubtypeLocale(getCurrentSubtype()); + return getCurrentSubtype().getLocales(); } - public InputMethodSubtype getCurrentSubtype() { + public RichInputMethodSubtype getCurrentSubtype() { if (null != sForcedSubtypeForTesting) { return sForcedSubtypeForTesting; } - return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype()); + return mCurrentRichInputMethodSubtype; } - public InputMethodSubtype getNoLanguageSubtype() { + public RichInputMethodSubtype getNoLanguageSubtype() { if (mNoLanguageSubtype == null) { - mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( - SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY); + mNoLanguageSubtype = new RichInputMethodSubtype( + mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY)); } if (mNoLanguageSubtype != null) { return mNoLanguageSubtype; @@ -313,10 +321,14 @@ public final class SubtypeSwitcher { return DUMMY_NO_LANGUAGE_SUBTYPE; } - public InputMethodSubtype getEmojiSubtype() { + public RichInputMethodSubtype getEmojiSubtype() { if (mEmojiSubtype == null) { - mEmojiSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( - SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI); + final InputMethodSubtype rawEmojiSubtype = + mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet( + SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI); + if (null != rawEmojiSubtype) { + mEmojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype); + } } if (mEmojiSubtype != null) { return mEmojiSubtype; @@ -328,6 +340,6 @@ public final class SubtypeSwitcher { } public String getCombiningRulesExtraValueOfCurrentSubtype() { - return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype()); + return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype()); } } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index b03818c1d..e181237a6 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -55,10 +55,6 @@ public final class Suggest { mDictionaryFacilitator = dictionaryFacilitator; } - public Locale getLocale() { - return mDictionaryFacilitator.getLocale(); - } - public void setAutoCorrectionThreshold(final float threshold) { mAutoCorrectionThreshold = threshold; } @@ -68,15 +64,15 @@ public final class Suggest { } public void getSuggestedWords(final WordComposer wordComposer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final NgramContext ngramContext, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final boolean isCorrectionEnabled, final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { if (wordComposer.isBatchMode()) { - getSuggestedWordsForBatchInput(wordComposer, prevWordsInfo, proximityInfo, + getSuggestedWordsForBatchInput(wordComposer, ngramContext, proximityInfo, settingsValuesForSuggestion, inputStyle, sequenceNumber, callback); } else { - getSuggestedWordsForNonBatchInput(wordComposer, prevWordsInfo, proximityInfo, + getSuggestedWordsForNonBatchInput(wordComposer, ngramContext, proximityInfo, settingsValuesForSuggestion, inputStyle, isCorrectionEnabled, sequenceNumber, callback); } @@ -84,7 +80,7 @@ public final class Suggest { private static ArrayList<SuggestedWordInfo> getTransformedSuggestedWordInfoList( final WordComposer wordComposer, final SuggestionResults results, - final int trailingSingleQuotesCount) { + final int trailingSingleQuotesCount, final Locale defaultLocale) { final boolean shouldMakeSuggestionsAllUpperCase = wordComposer.isAllUpperCase() && !wordComposer.isResumed(); final boolean isOnlyFirstCharCapitalized = @@ -96,9 +92,11 @@ public final class Suggest { || 0 != trailingSingleQuotesCount) { for (int i = 0; i < suggestionsCount; ++i) { final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); + final Locale wordLocale = wordInfo.mSourceDict.mLocale; final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo( - wordInfo, results.mLocale, shouldMakeSuggestionsAllUpperCase, - isOnlyFirstCharCapitalized, trailingSingleQuotesCount); + wordInfo, null == wordLocale ? defaultLocale : wordLocale, + shouldMakeSuggestionsAllUpperCase, isOnlyFirstCharCapitalized, + trailingSingleQuotesCount); suggestionsContainer.set(i, transformedWordInfo); } } @@ -119,7 +117,7 @@ public final class Suggest { // Retrieves suggestions for non-batch input (typing, recorrection, predictions...) // and calls the callback function with the suggestions. private void getSuggestedWordsForNonBatchInput(final WordComposer wordComposer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final NgramContext ngramContext, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int inputStyleIfNotPrediction, final boolean isCorrectionEnabled, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { @@ -130,11 +128,14 @@ public final class Suggest { : typedWord; final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( - wordComposer, prevWordsInfo, proximityInfo, settingsValuesForSuggestion, + wordComposer, ngramContext, proximityInfo, settingsValuesForSuggestion, SESSION_ID_TYPING); final ArrayList<SuggestedWordInfo> suggestionsContainer = getTransformedSuggestedWordInfoList(wordComposer, suggestionResults, - trailingSingleQuotesCount); + trailingSingleQuotesCount, + // For transforming suggestions that don't come for any dictionary, we + // use the currently most probable locale as it's our best bet. + mDictionaryFacilitator.getMostProbableLocale()); final boolean didRemoveTypedWord = SuggestedWordInfo.removeDups(wordComposer.getTypedWord(), suggestionsContainer); @@ -155,7 +156,7 @@ public final class Suggest { if (!isCorrectionEnabled || !allowsToBeAutoCorrected || resultsArePredictions || suggestionResults.isEmpty() || wordComposer.hasDigits() || wordComposer.isMostlyCaps() || wordComposer.isResumed() - || !mDictionaryFacilitator.hasInitializedMainDictionary() + || !mDictionaryFacilitator.hasAtLeastOneInitializedMainDictionary() || suggestionResults.first().isKindOf(SuggestedWordInfo.KIND_SHORTCUT)) { // If we don't have a main dictionary, we never want to auto-correct. The reason for // this is, the user may have a contact whose name happens to match a valid word in @@ -207,13 +208,15 @@ public final class Suggest { // Retrieves suggestions for the batch input // and calls the callback function with the suggestions. private void getSuggestedWordsForBatchInput(final WordComposer wordComposer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, + final NgramContext ngramContext, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( - wordComposer, prevWordsInfo, proximityInfo, settingsValuesForSuggestion, + wordComposer, ngramContext, proximityInfo, settingsValuesForSuggestion, SESSION_ID_GESTURE); + // For transforming words that don't come from a dictionary, because it's our best bet + final Locale defaultLocale = mDictionaryFacilitator.getMostProbableLocale(); final ArrayList<SuggestedWordInfo> suggestionsContainer = new ArrayList<>(suggestionResults); final int suggestionsCount = suggestionsContainer.size(); @@ -222,9 +225,10 @@ public final class Suggest { if (isFirstCharCapitalized || isAllUpperCase) { for (int i = 0; i < suggestionsCount; ++i) { final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); + final Locale wordlocale = wordInfo.mSourceDict.mLocale; final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo( - wordInfo, suggestionResults.mLocale, isAllUpperCase, isFirstCharCapitalized, - 0 /* trailingSingleQuotesCount */); + wordInfo, null == wordlocale ? defaultLocale : wordlocale, isAllUpperCase, + isFirstCharCapitalized, 0 /* trailingSingleQuotesCount */); suggestionsContainer.set(i, transformedWordInfo); } } @@ -237,7 +241,7 @@ public final class Suggest { SuggestedWordInfo.removeDups(null /* typedWord */, suggestionsContainer); // For some reason some suggestions with MIN_VALUE are making their way here. - // TODO: Find a more robust way to detect distractors. + // TODO: Find a more robust way to detect distracters. for (int i = suggestionsContainer.size() - 1; i >= 0; --i) { if (suggestionsContainer.get(i).mScore < SUPPRESS_SUGGEST_THRESHOLD) { suggestionsContainer.remove(i); diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 1d221b77f..e5bf25d5f 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -45,7 +45,7 @@ public class SuggestedWords { public static final int MAX_SUGGESTIONS = 18; private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = new ArrayList<>(0); - public static final SuggestedWords EMPTY = new SuggestedWords( + private static final SuggestedWords EMPTY = new SuggestedWords( EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false /* typedWordValid */, false /* willAutoCorrect */, false /* isObsoleteSuggestions */, INPUT_STYLE_NONE); @@ -113,6 +113,19 @@ public class SuggestedWords { } /** + * Get suggested word to show as suggestions to UI. + * + * @param shouldShowLxxSuggestionUi true if showing suggestion UI introduced in LXX and later. + * @return the count of suggested word to show as suggestions to UI. + */ + public int getWordCountToShow(final boolean shouldShowLxxSuggestionUi) { + if (isPrediction() || !shouldShowLxxSuggestionUi) { + return size(); + } + return size() - /* typed word */ 1; + } + + /** * Get suggested word at <code>index</code>. * @param index The index of the suggested word. * @return The suggested word. @@ -142,6 +155,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; @@ -187,6 +209,10 @@ public class SuggestedWords { return result; } + public static final SuggestedWords getEmptyInstance() { + return SuggestedWords.EMPTY; + } + // Should get rid of the first one (what the user typed previously) from suggestions // and replace it with what the user currently typed. public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions( @@ -217,7 +243,8 @@ public class SuggestedWords { return candidate.isEligibleForAutoCommit() ? candidate : null; } - public static final class SuggestedWordInfo { + // non-final for testability. + public static class SuggestedWordInfo { public static final int NOT_AN_INDEX = -1; public static final int NOT_A_CONFIDENCE = -1; public static final int MAX_SCORE = Integer.MAX_VALUE; @@ -387,28 +414,6 @@ public class SuggestedWords { return isPrediction(mInputStyle); } - // 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. - // This is only ever called by recorrection at the moment, hence the ForRecorrection moniker. - public SuggestedWords getSuggestedWordsExcludingTypedWordForRecorrection() { - final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>(); - String typedWord = null; - for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { - final SuggestedWordInfo info = mSuggestedWordInfoList.get(i); - if (!info.isKindOf(SuggestedWordInfo.KIND_TYPED)) { - newSuggestions.add(info); - } else { - assert(null == typedWord); - typedWord = info.mWord; - } - } - // 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, null /* rawSuggestions */, typedWord, - true /* typedWordValid */, false /* willAutoCorrect */, mIsObsoleteSuggestions, - SuggestedWords.INPUT_STYLE_RECORRECTION, NOT_A_SEQUENCE_NUMBER); - } - // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the // last word of all suggestions, separated by a space. This is necessary because when we commit // a multiple-word suggestion, the IME only retains the last word as the composing word, and diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 32d1fe372..5eb338eb3 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin; import com.android.inputmethod.event.CombinerChain; import com.android.inputmethod.event.Event; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.DebugFlags; import com.android.inputmethod.latin.utils.CoordinateUtils; import com.android.inputmethod.latin.utils.StringUtils; @@ -48,7 +49,7 @@ public final class WordComposer { // The list of events that served to compose this string. private final ArrayList<Event> mEvents; private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH); - private String mAutoCorrection; + private SuggestedWordInfo mAutoCorrection; private boolean mIsResumed; private boolean mIsBatchMode; // A memory of the last rejected batch mode suggestion, if any. This goes like this: the user @@ -127,8 +128,7 @@ public final class WordComposer { * Number of keystrokes in the composing word. * @return the number of keystrokes */ - // This may be made public if need be, but right now it's not used anywhere - /* package for tests */ int size() { + public int size() { return mCodePointSize; } @@ -418,14 +418,14 @@ public final class WordComposer { /** * Sets the auto-correction for this word. */ - public void setAutoCorrection(final String correction) { - mAutoCorrection = correction; + public void setAutoCorrection(final SuggestedWordInfo autoCorrection) { + mAutoCorrection = autoCorrection; } /** * @return the auto-correction for this word, or null if none. */ - public String getAutoCorrectionOrNull() { + public SuggestedWordInfo getAutoCorrectionOrNull() { return mAutoCorrection; } @@ -439,13 +439,13 @@ public final class WordComposer { // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above. // committedWord should contain suggestion spans if applicable. public LastComposedWord commitWord(final int type, final CharSequence committedWord, - final String separatorString, final PrevWordsInfo prevWordsInfo) { + final String separatorString, final NgramContext ngramContext) { // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate // the last composed word to ensure this does not happen. final LastComposedWord lastComposedWord = new LastComposedWord(mEvents, mInputPointers, mTypedWordCache.toString(), committedWord, separatorString, - prevWordsInfo, mCapitalizedMode); + ngramContext, mCapitalizedMode); mInputPointers.reset(); if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) { diff --git a/java/src/com/android/inputmethod/latin/accounts/AccountsChangedReceiver.java b/java/src/com/android/inputmethod/latin/accounts/AccountsChangedReceiver.java new file mode 100644 index 000000000..00bcecf52 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/accounts/AccountsChangedReceiver.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2014 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.accounts; + +import android.accounts.AccountManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.util.Log; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.settings.LocalSettingsConstants; + +/** + * {@link BroadcastReceiver} for {@link AccountManager#LOGIN_ACCOUNTS_CHANGED_ACTION}. + */ +public class AccountsChangedReceiver extends BroadcastReceiver { + static final String TAG = "AccountsChangedReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + if (!AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(intent.getAction())) { + Log.w(TAG, "Received unknown broadcast: " + intent); + return; + } + + // Ideally the account preference could live in a different preferences file + // that wasn't being backed up and restored, however the preference fragments + // currently only deal with the default shared preferences which is why + // separating this out into a different file is not trivial currently. + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final String currentAccount = prefs.getString( + LocalSettingsConstants.PREF_ACCOUNT_NAME, null); + removeUnknownAccountFromPreference(prefs, getAccountsForLogin(context), currentAccount); + } + + /** + * Helper method to help test this receiver. + */ + @UsedForTesting + protected String[] getAccountsForLogin(Context context) { + return LoginAccountUtils.getAccountsForLogin(context); + } + + /** + * Removes the currentAccount from preferences if it's not found + * in the list of current accounts. + */ + private static void removeUnknownAccountFromPreference(final SharedPreferences prefs, + final String[] accounts, final String currentAccount) { + if (currentAccount == null) { + return; + } + for (final String account : accounts) { + if (TextUtils.equals(currentAccount, account)) { + return; + } + } + Log.i(TAG, "The current account was removed from the system: " + currentAccount); + prefs.edit() + .remove(LocalSettingsConstants.PREF_ACCOUNT_NAME) + .apply(); + } +} diff --git a/java/src/com/android/inputmethod/latin/accounts/AuthUtils.java b/java/src/com/android/inputmethod/latin/accounts/AuthUtils.java new file mode 100644 index 000000000..31aba3631 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/accounts/AuthUtils.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2014 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.accounts; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; + +import java.io.IOException; + +/** + * Utility class that handles generation/invalidation of auth tokens in the app. + */ +public class AuthUtils { + private final AccountManager mAccountManager; + + public AuthUtils(Context context) { + mAccountManager = AccountManager.get(context); + } + + /** + * @see AccountManager#invalidateAuthToken(String, String) + */ + public void invalidateAuthToken(final String accountType, final String authToken) { + mAccountManager.invalidateAuthToken(accountType, authToken); + } + + /** + * @see AccountManager#getAuthToken( + * Account, String, Bundle, boolean, AccountManagerCallback, Handler) + */ + public AccountManagerFuture<Bundle> getAuthToken(final Account account, + final String authTokenType, final Bundle options, final boolean notifyAuthFailure, + final AccountManagerCallback<Bundle> callback, final Handler handler) { + return mAccountManager.getAuthToken(account, authTokenType, options, notifyAuthFailure, + callback, handler); + } + + /** + * @see AccountManager#blockingGetAuthToken(Account, String, boolean) + */ + public String blockingGetAuthToken(final Account account, final String authTokenType, + final boolean notifyAuthFailure) throws OperationCanceledException, + AuthenticatorException, IOException { + return mAccountManager.blockingGetAuthToken(account, authTokenType, notifyAuthFailure); + } +} diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java index fdab7f25f..5cc61db79 100644 --- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -45,7 +45,7 @@ import com.android.inputmethod.latin.DictionaryFacilitator; import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.LastComposedWord; import com.android.inputmethod.latin.LatinIME; -import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.NgramContext; import com.android.inputmethod.latin.RichInputConnection; import com.android.inputmethod.latin.Suggest; import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; @@ -53,7 +53,6 @@ import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; import com.android.inputmethod.latin.define.DebugFlags; -import com.android.inputmethod.latin.define.ProductionFlags; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; @@ -61,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; @@ -85,7 +85,7 @@ public final class InputLogic { // Current space state of the input method. This can be any of the above constants. private int mSpaceState; // Never null - public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; + public SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance(); public final Suggest mSuggest; private final DictionaryFacilitator mDictionaryFacilitator; @@ -144,13 +144,20 @@ public final class InputLogic { */ public void startInput(final String combiningSpec, final SettingsValues settingsValues) { mEnteredText = null; + if (!mWordComposer.getTypedWord().isEmpty()) { + // For messaging apps that offer send button, the IME does not get the opportunity + // to capture the last word. This block should capture those uncommitted words. + // The timestamp at which it is captured is not accurate but close enough. + StatsUtils.onWordCommitUserTyped( + mWordComposer.getTypedWord(), mWordComposer.isBatchMode()); + } mWordComposer.restartCombining(combiningSpec); resetComposingState(true /* alsoResetLastComposedWord */); mDeleteCount = 0; mSpaceState = SpaceState.NONE; mRecapitalizeStatus.disable(); // Do not perform recapitalize until the cursor is moved once mCurrentlyPressedHardwareKeys.clear(); - mSuggestedWords = SuggestedWords.EMPTY; + mSuggestedWords = SuggestedWords.getEmptyInstance(); // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying // so we try using some heuristics to find out about these and fix them. mConnection.tryFixLyingCursorPosition(); @@ -161,14 +168,11 @@ public final class InputLogic { mInputLogicHandler.reset(); } - if (ProductionFlags.ENABLE_CURSOR_ANCHOR_INFO_CALLBACK) { - // AcceptTypedWord feature relies on CursorAnchorInfo. - if (settingsValues.mShouldShowUiToAcceptTypedWord) { - mConnection.requestCursorUpdates(true /* enableMonitor */, - true /* requestImmediateCallback */); - } - mTextDecorator.reset(); + if (settingsValues.mShouldShowLxxSuggestionUi) { + mConnection.requestCursorUpdates(true /* enableMonitor */, + true /* requestImmediateCallback */); } + mTextDecorator.reset(); } /** @@ -204,6 +208,8 @@ public final class InputLogic { public void finishInput() { if (mWordComposer.isComposingWord()) { mConnection.finishComposingText(); + StatsUtils.onWordCommitUserTyped( + mWordComposer.getTypedWord(), mWordComposer.isBatchMode()); } resetComposingState(true /* alsoResetLastComposedWord */); mInputLogicHandler.reset(); @@ -250,6 +256,7 @@ public final class InputLogic { promotePhantomSpace(settingsValues); } mConnection.commitText(text, 1); + StatsUtils.onWordCommitUserTyped(mEnteredText, mWordComposer.isBatchMode()); mConnection.endBatchEdit(); // Space state must be updated before calling updateShiftState mSpaceState = SpaceState.NONE; @@ -298,6 +305,7 @@ public final class InputLogic { currentKeyboardScriptId, handler); } + mDictionaryFacilitator.switchMostProbableLanguage(suggestionInfo.mSourceDict.mLocale); final Event event = Event.createSuggestionPickedEvent(suggestionInfo); final InputTransaction inputTransaction = new InputTransaction(settingsValues, event, SystemClock.uptimeMillis(), mSpaceState, keyboardShiftState); @@ -321,7 +329,7 @@ public final class InputLogic { // however need to reset the suggestion strip right away, because we know we can't take // the risk of calling commitCompletion twice because we don't know how the app will react. if (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_APP_DEFINED)) { - mSuggestedWords = SuggestedWords.EMPTY; + mSuggestedWords = SuggestedWords.getEmptyInstance(); mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); resetComposingState(true /* alsoResetLastComposedWord */); @@ -341,12 +349,17 @@ public final class InputLogic { inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); if (shouldShowAddToDictionaryHint) { - mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion); + mSuggestionStripViewAccessor.suggestAddingToDictionary(suggestion, + true /* isFromSuggestionStrip */); } else { // If we're not showing the "Touch again to save", then update the suggestion strip. // That's going to be predictions (or punctuation suggestions), so INPUT_STYLE_NONE. handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE); } + + StatsUtils.onPickSuggestionManually(mSuggestedWords, suggestionInfo); + StatsUtils.onWordCommitSuggestionPickedManually( + suggestionInfo.mWord, mWordComposer.isBatchMode()); return inputTransaction; } @@ -422,8 +435,7 @@ public final class InputLogic { // removed. mConnection.removeBackgroundColorFromHighlightedTextIfNecessary(); // We moved the cursor. If we are touching a word, we need to resume suggestion. - mLatinIME.mHandler.postResumeSuggestions(false /* shouldIncludeResumedWordInSuggestions */, - true /* shouldDelay */); + mLatinIME.mHandler.postResumeSuggestions(true /* shouldDelay */); // Stop the last recapitalization, if started. mRecapitalizeStatus.stop(); return true; @@ -495,7 +507,7 @@ public final class InputLogic { final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { mInputLogicHandler.onStartBatchInput(); handler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); + SuggestedWords.getEmptyInstance(), false /* dismissGestureFloatingPreviewText */); handler.cancelUpdateSuggestionStrip(); ++mAutoCommitSequenceNumber; mConnection.beginBatchEdit(); @@ -572,6 +584,7 @@ public final class InputLogic { batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); promotePhantomSpace(settingsValues); mConnection.commitText(commitParts[0], 0); + StatsUtils.onWordCommitUserTyped(commitParts[0], mWordComposer.isBatchMode()); mSpaceState = SpaceState.PHANTOM; keyboardSwitcher.requestUpdatingShiftState( getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState()); @@ -593,23 +606,26 @@ public final class InputLogic { public void onCancelBatchInput(final LatinIME.UIHandler handler) { mInputLogicHandler.onCancelBatchInput(); handler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); + SuggestedWords.getEmptyInstance(), true /* dismissGestureFloatingPreviewText */); } // TODO: on the long term, this method should become private, but it will be difficult. // Especially, how do we deal with InputMethodService.onDisplayCompletions? - public void setSuggestedWords(final SuggestedWords suggestedWords, - final SettingsValues settingsValues, final LatinIME.UIHandler handler) { - if (SuggestedWords.EMPTY != suggestedWords) { - final String autoCorrection; + public void setSuggestedWords(final SuggestedWords suggestedWords) { + if (!suggestedWords.isEmpty()) { + final SuggestedWordInfo suggestedWordInfo; if (suggestedWords.mWillAutoCorrect) { - autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); + suggestedWordInfo = suggestedWords.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION); } else { // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD) // because it may differ from mWordComposer.mTypedWord. - autoCorrection = suggestedWords.mTypedWord; + suggestedWordInfo = new SuggestedWordInfo(suggestedWords.mTypedWord, + SuggestedWordInfo.MAX_SCORE, + SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); } - mWordComposer.setAutoCorrection(autoCorrection); + mWordComposer.setAutoCorrection(suggestedWordInfo); } mSuggestedWords = suggestedWords; final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; @@ -683,7 +699,7 @@ public final class InputLogic { break; case Constants.CODE_CAPSLOCK: // Note: Changing keyboard to shift lock state is handled in - // {@link KeyboardSwitcher#onCodeInput(int)}. + // {@link KeyboardSwitcher#onEvent(Event)}. break; case Constants.CODE_SYMBOL_SHIFT: // Note: Calling back to the keyboard on the symbol Shift key is handled in @@ -711,11 +727,11 @@ public final class InputLogic { break; case Constants.CODE_EMOJI: // Note: Switching emoji keyboard is being handled in - // {@link KeyboardState#onCodeInput(int,int)}. + // {@link KeyboardState#onEvent(Event,int)}. break; case Constants.CODE_ALPHA_FROM_EMOJI: // Note: Switching back from Emoji keyboard to the main keyboard is being - // handled in {@link KeyboardState#onCodeInput(int,int)}. + // handled in {@link KeyboardState#onEvent(Event,int)}. break; case Constants.CODE_SHIFT_ENTER: // TODO: remove this object @@ -1043,8 +1059,10 @@ public final class InputLogic { if (!TextUtils.isEmpty(rejectedSuggestion)) { mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion); } + StatsUtils.onBackspaceWordDelete(rejectedSuggestion.length()); } else { mWordComposer.applyProcessedEvent(event); + StatsUtils.onBackspacePressed(1); } if (mWordComposer.isComposingWord()) { setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); @@ -1054,7 +1072,10 @@ public final class InputLogic { inputTransaction.setRequiresUpdateSuggestions(); } else { if (mLastComposedWord.canRevertCommit()) { + final String lastComposedWord = mLastComposedWord.mTypedWord; revertCommit(inputTransaction, inputTransaction.mSettingsValues); + StatsUtils.onRevertAutoCorrect(); + StatsUtils.onWordCommitUserTyped(lastComposedWord, mWordComposer.isBatchMode()); return; } if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { @@ -1062,6 +1083,7 @@ public final class InputLogic { // This is triggered on backspace after a key that inputs multiple characters, // like the smiley key or the .com key. mConnection.deleteSurroundingText(mEnteredText.length(), 0); + StatsUtils.onDeleteMultiCharInput(mEnteredText.length()); mEnteredText = null; // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. // In addition we know that spaceState is false, and that we should not be @@ -1070,16 +1092,19 @@ 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(); mWordComposer.setCapitalizedModeAtStartComposingTime( WordComposer.CAPS_MODE_OFF); + StatsUtils.onRevertDoubleSpacePeriod(); return; } } else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { if (mConnection.revertSwapPunctuation()) { + StatsUtils.onRevertSwapPunctuation(); // Likewise return; } @@ -1094,25 +1119,31 @@ public final class InputLogic { mConnection.setSelection(mConnection.getExpectedSelectionEnd(), mConnection.getExpectedSelectionEnd()); mConnection.deleteSurroundingText(numCharsDeleted, 0); + StatsUtils.onBackspaceSelectedText(numCharsDeleted); } else { // There is no selection, just delete one character. - if (Constants.NOT_A_CURSOR_POSITION == mConnection.getExpectedSelectionEnd()) { - // This should never happen. - Log.e(TAG, "Backspace when we don't know the selection position"); - } - if (inputTransaction.mSettingsValues.isBeforeJellyBean() || - inputTransaction.mSettingsValues.mInputAttributes.isTypeNull()) { - // There are two possible reasons to send a key event: either the field has + if (inputTransaction.mSettingsValues.isBeforeJellyBean() + || inputTransaction.mSettingsValues.mInputAttributes.isTypeNull() + || Constants.NOT_A_CURSOR_POSITION + == mConnection.getExpectedSelectionEnd()) { + // There are three possible reasons to send a key event: either the field has // type TYPE_NULL, in which case the keyboard should send events, or we are - // running in backward compatibility mode. Before Jelly bean, the keyboard - // would simulate a hardware keyboard event on pressing enter or delete. This - // is bad for many reasons (there are race conditions with commits) but some - // applications are relying on this behavior so we continue to support it for - // older apps, so we retain this behavior if the app has target SDK < JellyBean. + // running in backward compatibility mode, or we don't know the cursor position. + // Before Jelly bean, the keyboard would simulate a hardware keyboard event on + // pressing enter or delete. This is bad for many reasons (there are race + // conditions with commits) but some applications are relying on this behavior + // so we continue to support it for older apps, so we retain this behavior if + // the app has target SDK < JellyBean. + // As for the case where we don't know the cursor position, it can happen + // because of bugs in the framework. But the framework should know, so the next + // best thing is to leave it to whatever it thinks is best. sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + int totalDeletedLength = 1; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + totalDeletedLength++; } + StatsUtils.onBackspacePressed(totalDeletedLength); } else { final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); if (codePointBeforeCursor == Constants.NOT_A_CODE) { @@ -1123,11 +1154,13 @@ public final class InputLogic { // catch it and have their broken interface react. If you need the keyboard // to do this, you're doing it wrong -- please fix your app. mConnection.deleteSurroundingText(1, 0); + // TODO: Add a new StatsUtils method onBackspaceWhenNoText() return; } final int lengthToDelete = Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; mConnection.deleteSurroundingText(lengthToDelete, 0); + int totalDeletedLength = lengthToDelete; if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { final int codePointBeforeCursorToDeleteAgain = mConnection.getCodePointBeforeCursor(); @@ -1135,8 +1168,10 @@ public final class InputLogic { final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( codePointBeforeCursorToDeleteAgain) ? 2 : 1; mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); + totalDeletedLength += lengthToDeleteAgain; } } + StatsUtils.onBackspacePressed(totalDeletedLength); } } if (inputTransaction.mSettingsValues @@ -1146,7 +1181,7 @@ public final class InputLogic { && !mConnection.isCursorFollowedByWordCharacter( inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues, - true /* shouldIncludeResumedWordInSuggestions */, currentKeyboardScriptId); + currentKeyboardScriptId); } } } @@ -1253,7 +1288,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 = @@ -1336,7 +1373,7 @@ public final class InputLogic { } private void performAdditionToUserHistoryDictionary(final SettingsValues settingsValues, - final String suggestion, final PrevWordsInfo prevWordsInfo) { + final String suggestion, final NgramContext ngramContext) { // If correction is not enabled, we don't add words to the user history dictionary. // That's to avoid unintended additions in some sensitive fields, or fields that // expect to receive non-words. @@ -1348,7 +1385,7 @@ public final class InputLogic { final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds( System.currentTimeMillis()); mDictionaryFacilitator.addToUserHistory(suggestion, wasAutoCapitalized, - prevWordsInfo, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive); + ngramContext, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive); } public void performUpdateSuggestionStripSync(final SettingsValues settingsValues, @@ -1360,7 +1397,7 @@ public final class InputLogic { + "requested!"); } // Clear the suggestions strip. - mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.EMPTY); + mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.getEmptyInstance()); return; } @@ -1400,12 +1437,10 @@ public final class InputLogic { * do nothing. * * @param settingsValues the current values of the settings. - * @param shouldIncludeResumedWordInSuggestions whether to include the word on which we resume * suggestions in the suggestion list. */ // TODO: make this private. public void restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues, - final boolean shouldIncludeResumedWordInSuggestions, // TODO: remove this argument, put it into settingsValues final int currentKeyboardScriptId) { // HACK: We may want to special-case some apps that exhibit bad behavior in case of @@ -1453,13 +1488,11 @@ public final class InputLogic { if (numberOfCharsInWordBeforeCursor > expectedCursorPosition) return; final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>(); final String typedWord = range.mWord.toString(); - if (shouldIncludeResumedWordInSuggestions) { - suggestions.add(new SuggestedWordInfo(typedWord, - SuggestedWords.MAX_SUGGESTIONS + 1, - SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); - } + suggestions.add(new SuggestedWordInfo(typedWord, + SuggestedWords.MAX_SUGGESTIONS + 1, + SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); if (!isResumableWord(settingsValues, typedWord)) { mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); return; @@ -1479,10 +1512,10 @@ public final class InputLogic { } } final int[] codePoints = StringUtils.toCodePointArray(typedWord); - // We want the previous word for suggestion. If we have chars in the word + // We want the context of preceding words for suggestion. If we have chars in the word // before the cursor, then we want the word before that, hence 2; otherwise, // we want the word immediately before the cursor, hence 1. - final PrevWordsInfo prevWordsInfo = getPrevWordsInfoFromNthPreviousWordForSuggestion( + final NgramContext ngramContext = getNgramContextFromNthPreviousWordForSuggestion( settingsValues.mSpacingAndPunctuations, 0 == numberOfCharsInWordBeforeCursor ? 1 : 2); mWordComposer.setComposingWord(codePoints, @@ -1492,31 +1525,14 @@ public final class InputLogic { mConnection.maybeMoveTheCursorAroundAndRestoreToWorkaroundABug(); mConnection.setComposingRegion(expectedCursorPosition - numberOfCharsInWordBeforeCursor, expectedCursorPosition + range.getNumberOfCharsInWordAfterCursor()); - if (suggestions.size() <= (shouldIncludeResumedWordInSuggestions ? 1 : 0)) { + if (suggestions.size() <= 1) { // If there weren't any suggestion spans on this word, suggestions#size() will be 1 // if shouldIncludeResumedWordInSuggestions is true, 0 otherwise. In this case, we // have no useful suggestions, so we will try to compute some for it instead. mInputLogicHandler.getSuggestedWords(Suggest.SESSION_ID_TYPING, SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { @Override - public void onGetSuggestedWords( - final SuggestedWords suggestedWordsIncludingTypedWord) { - final SuggestedWords suggestedWords; - if (suggestedWordsIncludingTypedWord.size() > 1 - && !shouldIncludeResumedWordInSuggestions) { - // 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 #getSuggestedWordsExcludingTypedWordForRecorrection() - // method sets willAutoCorrect to false. - suggestedWords = suggestedWordsIncludingTypedWord - .getSuggestedWordsExcludingTypedWordForRecorrection(); - } 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; - } + public void onGetSuggestedWords(final SuggestedWords suggestedWords) { mIsAutoCorrectionIndicatorOn = false; mLatinIME.mHandler.showSuggestionStrip(suggestedWords); }}); @@ -1551,6 +1567,10 @@ public final class InputLogic { final String committedWordString = committedWord.toString(); final int cancelLength = committedWord.length(); final String separatorString = mLastComposedWord.mSeparatorString; + // If our separator is a space, we won't actually commit it, + // but set the space state to PHANTOM so that a space will be inserted + // on the next keypress + final boolean usePhantomSpace = separatorString.equals(Constants.STRING_SPACE); // We want java chars, not codepoints for the following. final int separatorLength = separatorString.length(); // TODO: should we check our saved separator against the actual contents of the text view? @@ -1571,7 +1591,8 @@ public final class InputLogic { if (!TextUtils.isEmpty(committedWord)) { mDictionaryFacilitator.removeWordFromPersonalizedDicts(committedWordString); } - final String stringToCommit = originallyTypedWord + separatorString; + final String stringToCommit = originallyTypedWord + + (usePhantomSpace ? "" : separatorString); final SpannableString textToCommit = new SpannableString(stringToCommit); if (committedWord instanceof SpannableString) { final SpannableString committedWordWithSuggestionSpans = (SpannableString)committedWord; @@ -1604,8 +1625,10 @@ public final class InputLogic { } } // Add the suggestion list to the list of suggestions. - textToCommit.setSpan(new SuggestionSpan(inputTransaction.mSettingsValues.mLocale, - suggestions.toArray(new String[suggestions.size()]), 0 /* flags */), + textToCommit.setSpan(new SuggestionSpan(mLatinIME /* context */, + inputTransaction.mSettingsValues.mLocale, + suggestions.toArray(new String[suggestions.size()]), 0 /* flags */, + null /* notificationTargetClass */), 0 /* start */, lastCharIndex /* end */, 0 /* flags */); } @@ -1623,6 +1646,9 @@ public final class InputLogic { } else { mConnection.commitText(textToCommit, 1); } + if (usePhantomSpace) { + mSpaceState = SpaceState.PHANTOM; + } } else { // For languages without spaces, we revert the typed string but the cursor is flush // with the typed word, so we need to resume suggestions right away. @@ -1650,7 +1676,8 @@ public final class InputLogic { mConnection.getExpectedSelectionStart(), mConnection.getExpectedSelectionEnd()); } - mSuggestionStripViewAccessor.showAddToDictionaryHint(originallyTypedWordString); + mSuggestionStripViewAccessor.suggestAddingToDictionary(originallyTypedWordString, + false /* isFromSuggestionStrip */); } else { // We have a separator between the word and the cursor: we should show predictions. inputTransaction.setRequiresUpdateSuggestions(); @@ -1720,24 +1747,24 @@ public final class InputLogic { } /** - * Get information fo previous words from the nth previous word before the cursor as context + * Get n-gram context from the nth previous word before the cursor as context * for the suggestion process. * @param spacingAndPunctuations the current spacing and punctuations settings. * @param nthPreviousWord reverse index of the word to get (1-indexed) * @return the information of previous words */ // TODO: Make this private - public PrevWordsInfo getPrevWordsInfoFromNthPreviousWordForSuggestion( + public NgramContext getNgramContextFromNthPreviousWordForSuggestion( final SpacingAndPunctuations spacingAndPunctuations, final int nthPreviousWord) { if (spacingAndPunctuations.mCurrentLanguageHasSpaces) { // If we are typing in a language with spaces we can just look up the previous // word information from textview. - return mConnection.getPrevWordsInfoFromNthPreviousWord( + return mConnection.getNgramContextFromNthPreviousWord( spacingAndPunctuations, nthPreviousWord); } else { return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? - PrevWordsInfo.BEGINNING_OF_SENTENCE : - new PrevWordsInfo(new PrevWordsInfo.WordInfo( + NgramContext.BEGINNING_OF_SENTENCE : + new NgramContext(new NgramContext.WordInfo( mLastComposedWord.mCommittedWord.toString())); } } @@ -1852,9 +1879,8 @@ public final class InputLogic { */ private SuggestedWords retrieveOlderSuggestions(final String typedWord, final SuggestedWords previousSuggestedWords) { - final SuggestedWords oldSuggestedWords = - previousSuggestedWords.isPunctuationSuggestions() ? SuggestedWords.EMPTY - : previousSuggestedWords; + final SuggestedWords oldSuggestedWords = previousSuggestedWords.isPunctuationSuggestions() + ? SuggestedWords.getEmptyInstance() : previousSuggestedWords; final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords); return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */, @@ -1976,6 +2002,8 @@ public final class InputLogic { final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; if (0 != indexOfLastSpace) { mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1); + StatsUtils.onWordCommitUserTyped( + batchInputText.substring(0, indexOfLastSpace), mWordComposer.isBatchMode()); final SuggestedWords suggestedWordsForLastWordOfPhraseGesture = suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture(); mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture); @@ -2014,8 +2042,10 @@ public final class InputLogic { if (!mWordComposer.isComposingWord()) return; final String typedWord = mWordComposer.getTypedWord(); if (typedWord.length() > 0) { + final boolean isBatchMode = mWordComposer.isBatchMode(); commitChosenWord(settingsValues, typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString); + StatsUtils.onWordCommitUserTyped(typedWord, isBatchMode); } } @@ -2052,18 +2082,23 @@ public final class InputLogic { // INPUT_STYLE_TYPING. performUpdateSuggestionStripSync(settingsValues, SuggestedWords.INPUT_STYLE_TYPING); } - final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); + final SuggestedWordInfo autoCorrectionOrNull = mWordComposer.getAutoCorrectionOrNull(); final String typedWord = mWordComposer.getTypedWord(); - final String autoCorrection = (typedAutoCorrection != null) - ? typedAutoCorrection : typedWord; - if (autoCorrection != null) { + final String stringToCommit = (autoCorrectionOrNull != null) + ? autoCorrectionOrNull.mWord : typedWord; + if (stringToCommit != null) { if (TextUtils.isEmpty(typedWord)) { throw new RuntimeException("We have an auto-correction but the typed word " + "is empty? Impossible! I must commit suicide."); } - commitChosenWord(settingsValues, autoCorrection, + final boolean isBatchMode = mWordComposer.isBatchMode(); + commitChosenWord(settingsValues, stringToCommit, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator); - if (!typedWord.equals(autoCorrection)) { + if (null != autoCorrectionOrNull) { + mDictionaryFacilitator.switchMostProbableLanguage( + autoCorrectionOrNull.mSourceDict.mLocale); + } + if (!typedWord.equals(stringToCommit)) { // This will make the correction flash for a short while as a visual clue // to the user that auto-correction happened. It has no other effect; in particular // note that this won't affect the text inside the text field AT ALL: it only makes @@ -2071,8 +2106,14 @@ public final class InputLogic { // of the auto-correction flash. At this moment, the "typedWord" argument is // ignored by TextView. mConnection.commitCorrection(new CorrectionInfo( - mConnection.getExpectedSelectionEnd() - autoCorrection.length(), - typedWord, autoCorrection)); + mConnection.getExpectedSelectionEnd() - stringToCommit.length(), + typedWord, stringToCommit)); + StatsUtils.onAutoCorrection(typedWord, stringToCommit, isBatchMode, + null == autoCorrectionOrNull + ? null : autoCorrectionOrNull.mSourceDict.mDictType); + StatsUtils.onWordCommitAutoCorrect(stringToCommit, isBatchMode); + } else { + StatsUtils.onWordCommitUserTyped(stringToCommit, isBatchMode); } } } @@ -2091,20 +2132,20 @@ public final class InputLogic { final CharSequence chosenWordWithSuggestions = SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, suggestedWords); - // When we are composing word, get previous words information from the 2nd previous word - // because the 1st previous word is the word to be committed. Otherwise get previous words - // information from the 1st previous word. - final PrevWordsInfo prevWordsInfo = mConnection.getPrevWordsInfoFromNthPreviousWord( + // When we are composing word, get n-gram context from the 2nd previous word because the + // 1st previous word is the word to be committed. Otherwise get n-gram context from the 1st + // previous word. + final NgramContext ngramContext = mConnection.getNgramContextFromNthPreviousWord( settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1); mConnection.commitText(chosenWordWithSuggestions, 1); // Add the word to the user history dictionary - performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWordsInfo); + performAdditionToUserHistoryDictionary(settingsValues, chosenWord, ngramContext); // TODO: figure out here if this is an auto-correct or if the best word is actually // what user typed. Note: currently this is done much later in // LastComposedWord#didCommitTypedWord by string equality of the remembered // strings. mLastComposedWord = mWordComposer.commitWord(commitType, - chosenWordWithSuggestions, separatorString, prevWordsInfo); + chosenWordWithSuggestions, separatorString, ngramContext); } /** @@ -2137,10 +2178,7 @@ public final class InputLogic { } mConnection.tryFixLyingCursorPosition(); if (tryResumeSuggestions) { - // This is triggered when starting input anew, so we want to include the resumed - // word in suggestions. - handler.postResumeSuggestions(true /* shouldIncludeResumedWordInSuggestions */, - true /* shouldDelay */); + handler.postResumeSuggestions(true /* shouldDelay */); } return true; } @@ -2151,7 +2189,7 @@ public final class InputLogic { mWordComposer.adviseCapitalizedModeBeforeFetchingSuggestions( getActualCapsMode(settingsValues, keyboardShiftMode)); mSuggest.getSuggestedWords(mWordComposer, - getPrevWordsInfoFromNthPreviousWordForSuggestion( + getNgramContextFromNthPreviousWordForSuggestion( settingsValues.mSpacingAndPunctuations, // Get the word on which we should search the bigrams. If we are composing // a word, it's whatever is *before* the half-committed word in the buffer, @@ -2216,6 +2254,47 @@ public final class InputLogic { mConnection.setComposingText(composingTextToBeSet, newCursorPosition); } + /** + * Gets an object allowing private IME commands to be sent to the + * underlying editor. + * @return An object for sending private commands to the underlying editor. + */ + public PrivateCommandPerformer getPrivateCommandPerformer() { + return mConnection; + } + + /** + * Gets the expected index of the first char of the composing span within the editor's text. + * Returns a negative value in case there appears to be no valid composing span. + * + * @see #getComposingLength() + * @see RichInputConnection#hasSelection() + * @see RichInputConnection#isCursorPositionKnown() + * @see RichInputConnection#getExpectedSelectionStart() + * @see RichInputConnection#getExpectedSelectionEnd() + * @return The expected index in Java chars of the first char of the composing span. + */ + // TODO: try and see if we can get rid of this method. Ideally the users of this class should + // never need to know this. + public int getComposingStart() { + if (!mConnection.isCursorPositionKnown() || mConnection.hasSelection()) { + return -1; + } + return mConnection.getExpectedSelectionStart() - mWordComposer.size(); + } + + /** + * Gets the expected length in Java chars of the composing span. + * May be 0 if there is no valid composing span. + * @see #getComposingStart() + * @return The expected length of the composing span. + */ + // TODO: try and see if we can get rid of this method. Ideally the users of this class should + // never need to know this. + public int getComposingLength() { + return mWordComposer.size(); + } + ////////////////////////////////////////////////////////////////////////////////////////////// // Following methods are tentatively placed in this class for the integration with // TextDecorator. @@ -2267,7 +2346,7 @@ public final class InputLogic { // We cannot help in this case because we are heavily relying on this new API. return false; } - if (!settingsValues.mShouldShowUiToAcceptTypedWord) { + if (!settingsValues.mShouldShowLxxSuggestionUi) { return false; } if (TextUtils.isEmpty(lastComposedWord.mTypedWord)) { diff --git a/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java b/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java new file mode 100644 index 000000000..42eaa9c82 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/inputlogic/PrivateCommandPerformer.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 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.inputlogic; + +import android.os.Bundle; + +/** + * Provides an interface matching + * {@link android.view.inputmethod.InputConnection#performPrivateCommand(String,Bundle)}. + */ +public interface PrivateCommandPerformer { + /** + * API to send private commands from an input method to its connected + * editor. This can be used to provide domain-specific features that are + * only known between certain input methods and their clients. + * + * @param action Name of the command to be performed. This must be a scoped + * name, i.e. prefixed with a package name you own, so that + * different developers will not create conflicting commands. + * @param data Any data to include with the command. + * @return true if the command was sent (regardless of whether the + * associated editor understood it), false if the input connection is no + * longer valid. + */ + boolean performPrivateCommand(String action, Bundle data); +} diff --git a/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java b/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java index df447fd75..fa7c2c417 100644 --- a/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java +++ b/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java @@ -38,15 +38,12 @@ public final class DictionaryHeader { public static final String DICTIONARY_DATE_KEY = "date"; public static final String HAS_HISTORICAL_INFO_KEY = "HAS_HISTORICAL_INFO"; public static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE"; - public static final String FORGETTING_CURVE_OCCURRENCES_TO_LEVEL_UP_KEY = - "FORGETTING_CURVE_OCCURRENCES_TO_LEVEL_UP"; public static final String FORGETTING_CURVE_PROBABILITY_VALUES_TABLE_ID_KEY = "FORGETTING_CURVE_PROBABILITY_VALUES_TABLE_ID"; - public static final String FORGETTING_CURVE_DURATION_TO_LEVEL_DOWN_IN_SECONDS_KEY = - "FORGETTING_CURVE_DURATION_TO_LEVEL_DOWN_IN_SECONDS"; public static final String MAX_UNIGRAM_COUNT_KEY = "MAX_UNIGRAM_COUNT"; public static final String MAX_BIGRAM_COUNT_KEY = "MAX_BIGRAM_COUNT"; public static final String ATTRIBUTE_VALUE_TRUE = "1"; + public static final String CODE_POINT_TABLE_KEY = "codePointTable"; public DictionaryHeader(final int headerSize, final DictionaryOptions dictionaryOptions, final FormatOptions formatOptions) throws UnsupportedFormatException { diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java index a2ae74b20..34edfa0da 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java +++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java @@ -36,9 +36,7 @@ public final class FormatSpec { * sion * * o | - * p | not used 3 bits - * t | each unigram and bigram entry has a time stamp? - * i | 1 bit, 1 = yes, 0 = no : CONTAINS_TIMESTAMP_FLAG + * p | not used, 2 bytes. * o | * nflags * @@ -48,7 +46,7 @@ public final class FormatSpec { * d | * ersize * - * | attributes list + * attributes list * * attributes list is: * <key> = | string of characters at the char format described below, with the terminator used @@ -86,11 +84,10 @@ public final class FormatSpec { */ /* Node (FusionDictionary.PtNode) layout is as follows: - * | is moved ? 2 bits, 11 = no : FLAG_IS_NOT_MOVED - * | This must be the same as FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES - * | 01 = yes : FLAG_IS_MOVED - * f | the new address is stored in the same place as the parent address - * l | is deleted? 10 = yes : FLAG_IS_DELETED + * | CHILDREN_ADDRESS_TYPE 2 bits, 11 : FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES + * | 10 : FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES + * f | 01 : FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE + * l | 00 : FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS * a | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS * g | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL * s | has shortcut targets ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_SHORTCUT_TARGETS @@ -98,16 +95,6 @@ public final class FormatSpec { * | is not a word ? 1 bit, 1 = yes, 0 = no : FLAG_IS_NOT_A_WORD * | is blacklisted ? 1 bit, 1 = yes, 0 = no : FLAG_IS_BLACKLISTED * - * p | - * a | parent address, 3byte - * r | 1 byte = bbbbbbbb match - * e | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte) - * n | otherwise => (bbbbbbbb << 16) + (next byte << 8) + next byte - * t | This address is relative to the head of the PtNode. - * a | If the node doesn't have a parent, this field is set to 0. - * d | - * dress - * * c | IF FLAG_HAS_MULTIPLE_CHARS * h | char, char, char, char n * (1 or 3 bytes) : use PtNodeInfo for i/o helpers * a | end 1 byte, = 0 @@ -121,15 +108,10 @@ public final class FormatSpec { * q | * * c | - * h | children address, 3 bytes - * i | 1 byte = bbbbbbbb match - * l | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte) - * d | otherwise => (bbbbbbbb<<16) + (next byte << 8) + next byte - * r | if this node doesn't have children, this field is set to 0. - * e | (see BinaryDictEncoderUtils#writeVariableSignedAddress) - * n | This address is relative to the position of this field. - * a | - * ddress + * h | children address, CHILDREN_ADDRESS_TYPE bytes + * i | This address is relative to the position of this field. + * l | + * drenaddress * * | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS * | shortcut string list @@ -179,17 +161,17 @@ public final class FormatSpec { public static final int MAGIC_NUMBER = 0x9BC13AFE; static final int NOT_A_VERSION_NUMBER = -1; - static final int FIRST_VERSION_WITH_DYNAMIC_UPDATE = 3; - static final int FIRST_VERSION_WITH_TERMINAL_ID = 4; // These MUST have the same values as the relevant constants in format_utils.h. - // From version 4 on, we use version * 100 + revision as a version number. That allows + // From version 2.01 on, we use version * 100 + revision as a version number. That allows // us to change the format during development while having testing devices remove // older files with each upgrade, while still having a readable versioning scheme. // When we bump up the dictionary format version, we should update // ExpandableDictionary.needsToMigrateDictionary() and // ExpandableDictionary.matchesExpectedBinaryDictFormatVersionForThisType(). public static final int VERSION2 = 2; + public static final int VERSION201 = 201; + public static final int MINIMUM_SUPPORTED_VERSION_OF_CODE_POINT_TABLE = VERSION201; // Dictionary version used for testing. public static final int VERSION4_ONLY_FOR_TESTING = 399; public static final int VERSION401 = 401; @@ -202,9 +184,6 @@ public final class FormatSpec { // use it in the reading code. static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; - static final int PARENT_ADDRESS_SIZE = 3; - static final int FORWARD_LINK_ADDRESS_SIZE = 3; - // These flags are used only in the static dictionary. static final int MASK_CHILDREN_ADDRESS_TYPE = 0xC0; static final int FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS = 0x00; @@ -220,13 +199,6 @@ public final class FormatSpec { static final int FLAG_IS_NOT_A_WORD = 0x02; static final int FLAG_IS_BLACKLISTED = 0x01; - // These flags are used only in the dynamic dictionary. - static final int MASK_MOVE_AND_DELETE_FLAG = 0xC0; - static final int FIXED_BIT_OF_DYNAMIC_UPDATE_MOVE = 0x40; - static final int FLAG_IS_MOVED = 0x00 | FIXED_BIT_OF_DYNAMIC_UPDATE_MOVE; - static final int FLAG_IS_NOT_MOVED = 0x80 | FIXED_BIT_OF_DYNAMIC_UPDATE_MOVE; - static final int FLAG_IS_DELETED = 0x80; - static final int FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT = 0x80; static final int FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE = 0x40; static final int MASK_BIGRAM_ATTR_ADDRESS_TYPE = 0x30; @@ -240,52 +212,12 @@ public final class FormatSpec { static final int PTNODE_TERMINATOR_SIZE = 1; static final int PTNODE_FLAGS_SIZE = 1; static final int PTNODE_FREQUENCY_SIZE = 1; - static final int PTNODE_TERMINAL_ID_SIZE = 4; static final int PTNODE_MAX_ADDRESS_SIZE = 3; static final int PTNODE_ATTRIBUTE_FLAGS_SIZE = 1; static final int PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE = 3; static final int PTNODE_SHORTCUT_LIST_SIZE_SIZE = 2; - // These values are used only by version 4 or later. They MUST match the definitions in - // ver4_dict_constants.cpp. - static final String TRIE_FILE_EXTENSION = ".trie"; - public static final String HEADER_FILE_EXTENSION = ".header"; - static final String FREQ_FILE_EXTENSION = ".freq"; - // tat = Terminal Address Table - static final String TERMINAL_ADDRESS_TABLE_FILE_EXTENSION = ".tat"; - static final String BIGRAM_FILE_EXTENSION = ".bigram"; - static final String SHORTCUT_FILE_EXTENSION = ".shortcut"; - static final String LOOKUP_TABLE_FILE_SUFFIX = "_lookup"; - static final String CONTENT_TABLE_FILE_SUFFIX = "_index"; - static final int FLAGS_IN_FREQ_FILE_SIZE = 1; - static final int FREQUENCY_AND_FLAGS_SIZE = 2; - static final int TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE = 3; - static final int UNIGRAM_TIMESTAMP_SIZE = 4; - static final int UNIGRAM_COUNTER_SIZE = 1; - static final int UNIGRAM_LEVEL_SIZE = 1; - - // With the English main dictionary as of October 2013, the size of bigram address table is - // is 345KB with the block size being 16. - // This is 54% of that of full address table. - static final int BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 16; - static final int BIGRAM_CONTENT_COUNT = 1; - static final int BIGRAM_FREQ_CONTENT_INDEX = 0; - static final String BIGRAM_FREQ_CONTENT_ID = "_freq"; - static final int BIGRAM_TIMESTAMP_SIZE = 4; - static final int BIGRAM_COUNTER_SIZE = 1; - static final int BIGRAM_LEVEL_SIZE = 1; - - static final int SHORTCUT_CONTENT_COUNT = 1; - static final int SHORTCUT_CONTENT_INDEX = 0; - // With the English main dictionary as of October 2013, the size of shortcut address table is - // 26KB with the block size being 64. - // This is only 4.4% of that of full address table. - static final int SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE = 64; - static final String SHORTCUT_CONTENT_ID = "_shortcut"; - static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE; - static final int NO_PARENT_ADDRESS = 0; - static final int NO_FORWARD_LINK_ADDRESS = 0; static final int INVALID_CHARACTER = -1; static final int MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT = 0x7F; // 127 @@ -302,14 +234,13 @@ public final class FormatSpec { // This option needs to be the same numeric value as the one in binary_format.h. static final int NOT_VALID_WORD = -99; - static final int SIGNED_CHILDREN_ADDRESS_SIZE = 3; static final int UINT8_MAX = 0xFF; static final int UINT16_MAX = 0xFFFF; static final int UINT24_MAX = 0xFFFFFF; - static final int SINT24_MAX = 0x7FFFFF; static final int MSB8 = 0x80; - static final int MSB24 = 0x800000; + static final int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20; + static final int MAXIMAL_ONE_BYTE_CHARACTER_VALUE = 0xFF; /** * Options about file format. diff --git a/java/src/com/android/inputmethod/latin/makedict/NgramProperty.java b/java/src/com/android/inputmethod/latin/makedict/NgramProperty.java new file mode 100644 index 000000000..99e0e273f --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/NgramProperty.java @@ -0,0 +1,26 @@ +package com.android.inputmethod.latin.makedict; + +import com.android.inputmethod.latin.NgramContext; + +public class NgramProperty { + public final WeightedString mTargetWord; + public final NgramContext mNgramContext; + + public NgramProperty(final WeightedString targetWord, final NgramContext ngramContext) { + mTargetWord = targetWord; + mNgramContext = ngramContext; + } + + @Override + public int hashCode() { + return mTargetWord.hashCode() ^ mNgramContext.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof NgramProperty)) return false; + final NgramProperty n = (NgramProperty)o; + return mTargetWord.equals(n.mTargetWord) && mNgramContext.equals(n.mNgramContext); + } +} diff --git a/java/src/com/android/inputmethod/latin/makedict/WordProperty.java b/java/src/com/android/inputmethod/latin/makedict/WordProperty.java index cd78e2235..1e6cadf03 100644 --- a/java/src/com/android/inputmethod/latin/makedict/WordProperty.java +++ b/java/src/com/android/inputmethod/latin/makedict/WordProperty.java @@ -18,12 +18,16 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.BinaryDictionary; +import com.android.inputmethod.latin.NgramContext; +import com.android.inputmethod.latin.NgramContext.WordInfo; import com.android.inputmethod.latin.utils.CombinedFormatUtils; import com.android.inputmethod.latin.utils.StringUtils; import java.util.ArrayList; import java.util.Arrays; +import javax.annotation.Nullable; + /** * Utility class for a word with a probability. * @@ -33,29 +37,40 @@ public final class WordProperty implements Comparable<WordProperty> { public final String mWord; public final ProbabilityInfo mProbabilityInfo; public final ArrayList<WeightedString> mShortcutTargets; - public final ArrayList<WeightedString> mBigrams; + public final ArrayList<NgramProperty> mNgrams; // TODO: Support mIsBeginningOfSentence. public final boolean mIsBeginningOfSentence; public final boolean mIsNotAWord; public final boolean mIsBlacklistEntry; public final boolean mHasShortcuts; - public final boolean mHasBigrams; + public final boolean mHasNgrams; private int mHashCode = 0; + // TODO: Support n-gram. @UsedForTesting public WordProperty(final String word, final ProbabilityInfo probabilityInfo, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams, + @Nullable final ArrayList<WeightedString> bigrams, final boolean isNotAWord, final boolean isBlacklistEntry) { mWord = word; mProbabilityInfo = probabilityInfo; mShortcutTargets = shortcutTargets; - mBigrams = bigrams; + if (null == bigrams) { + mNgrams = null; + } else { + mNgrams = new ArrayList<>(); + final NgramContext ngramContext = new NgramContext(new WordInfo(mWord)); + if (bigrams != null) { + for (final WeightedString bigramTarget : bigrams) { + mNgrams.add(new NgramProperty(bigramTarget, ngramContext)); + } + } + } mIsBeginningOfSentence = false; mIsNotAWord = isNotAWord; mIsBlacklistEntry = isBlacklistEntry; - mHasBigrams = bigrams != null && !bigrams.isEmpty(); + mHasNgrams = bigrams != null && !bigrams.isEmpty(); mHasShortcuts = shortcutTargets != null && !shortcutTargets.isEmpty(); } @@ -72,26 +87,34 @@ public final class WordProperty implements Comparable<WordProperty> { public WordProperty(final int[] codePoints, final boolean isNotAWord, final boolean isBlacklisted, final boolean hasBigram, final boolean hasShortcuts, final boolean isBeginningOfSentence, final int[] probabilityInfo, - final ArrayList<int[]> bigramTargets, final ArrayList<int[]> bigramProbabilityInfo, + final ArrayList<int[][]> ngramPrevWordsArray, + final ArrayList<boolean[]> outNgramPrevWordIsBeginningOfSentenceArray, + final ArrayList<int[]> ngramTargets, final ArrayList<int[]> ngramProbabilityInfo, final ArrayList<int[]> shortcutTargets, final ArrayList<Integer> shortcutProbabilities) { mWord = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints); mProbabilityInfo = createProbabilityInfoFromArray(probabilityInfo); mShortcutTargets = new ArrayList<>(); - mBigrams = new ArrayList<>(); + final ArrayList<NgramProperty> ngrams = new ArrayList<>(); mIsBeginningOfSentence = isBeginningOfSentence; mIsNotAWord = isNotAWord; mIsBlacklistEntry = isBlacklisted; mHasShortcuts = hasShortcuts; - mHasBigrams = hasBigram; - - final int bigramTargetCount = bigramTargets.size(); - for (int i = 0; i < bigramTargetCount; i++) { - final String bigramTargetString = - StringUtils.getStringFromNullTerminatedCodePointArray(bigramTargets.get(i)); - mBigrams.add(new WeightedString(bigramTargetString, - createProbabilityInfoFromArray(bigramProbabilityInfo.get(i)))); + mHasNgrams = hasBigram; + + final int relatedNgramCount = ngramTargets.size(); + final WordInfo currentWordInfo = + mIsBeginningOfSentence ? WordInfo.BEGINNING_OF_SENTENCE : new WordInfo(mWord); + final NgramContext ngramContext = new NgramContext(currentWordInfo); + for (int i = 0; i < relatedNgramCount; i++) { + final String ngramTargetString = + StringUtils.getStringFromNullTerminatedCodePointArray(ngramTargets.get(i)); + final WeightedString ngramTarget = new WeightedString(ngramTargetString, + createProbabilityInfoFromArray(ngramProbabilityInfo.get(i))); + // TODO: Support n-gram. + ngrams.add(new NgramProperty(ngramTarget, ngramContext)); } + mNgrams = ngrams.isEmpty() ? null : ngrams; final int shortcutTargetCount = shortcutTargets.size(); for (int i = 0; i < shortcutTargetCount; i++) { @@ -102,6 +125,20 @@ public final class WordProperty implements Comparable<WordProperty> { } } + // TODO: Remove + public ArrayList<WeightedString> getBigrams() { + if (null == mNgrams) { + return null; + } + final ArrayList<WeightedString> bigrams = new ArrayList<>(); + for (final NgramProperty ngram : mNgrams) { + if (ngram.mNgramContext.getPrevWordCount() == 1) { + bigrams.add(ngram.mTargetWord); + } + } + return bigrams; + } + public int getProbability() { return mProbabilityInfo.mProbability; } @@ -110,8 +147,8 @@ public final class WordProperty implements Comparable<WordProperty> { return Arrays.hashCode(new Object[] { word.mWord, word.mProbabilityInfo, - word.mShortcutTargets.hashCode(), - word.mBigrams.hashCode(), + word.mShortcutTargets, + word.mNgrams, word.mIsNotAWord, word.mIsBlacklistEntry }); @@ -142,9 +179,17 @@ public final class WordProperty implements Comparable<WordProperty> { if (!(o instanceof WordProperty)) return false; WordProperty w = (WordProperty)o; return mProbabilityInfo.equals(w.mProbabilityInfo) && mWord.equals(w.mWord) - && mShortcutTargets.equals(w.mShortcutTargets) && mBigrams.equals(w.mBigrams) + && mShortcutTargets.equals(w.mShortcutTargets) && equals(mNgrams, w.mNgrams) && mIsNotAWord == w.mIsNotAWord && mIsBlacklistEntry == w.mIsBlacklistEntry - && mHasBigrams == w.mHasBigrams && mHasShortcuts && w.mHasBigrams; + && mHasNgrams == w.mHasNgrams && mHasShortcuts && w.mHasNgrams; + } + + // TDOO: Have a utility method like java.util.Objects.equals. + private static <T> boolean equals(final ArrayList<T> a, final ArrayList<T> b) { + if (null == a) { + return null == b; + } + return a.equals(b); } @Override diff --git a/java/src/com/android/inputmethod/latin/network/AuthException.java b/java/src/com/android/inputmethod/latin/network/AuthException.java new file mode 100644 index 000000000..1bce4c156 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/network/AuthException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 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.network; + +/** + * Authentication exception. When this exception is thrown, the client may + * try to refresh the authentication token and try again. + */ +public class AuthException extends Exception { + public AuthException() { + super(); + } + + public AuthException(Throwable throwable) { + super(throwable); + } + + public AuthException(String detailMessage) { + super(detailMessage); + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/network/BlockingHttpClient.java b/java/src/com/android/inputmethod/latin/network/BlockingHttpClient.java new file mode 100644 index 000000000..e2d24fd0a --- /dev/null +++ b/java/src/com/android/inputmethod/latin/network/BlockingHttpClient.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2014 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.network; + +import android.util.Log; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A client for executing HTTP requests synchronously. + * This must never be called from the main thread. + */ +public class BlockingHttpClient { + private static final boolean DEBUG = false; + private static final String TAG = BlockingHttpClient.class.getSimpleName(); + + private final HttpURLConnection mConnection; + + /** + * Interface that handles processing the response for a request. + */ + public interface ResponseProcessor<T> { + /** + * Called when the HTTP request finishes successfully. + * The {@link InputStream} is closed by the client after the method finishes, + * so any processing must be done in this method itself. + * + * @param response An input stream that can be used to read the HTTP response. + */ + T onSuccess(InputStream response) throws IOException; + } + + public BlockingHttpClient(HttpURLConnection connection) { + mConnection = connection; + } + + /** + * Executes the request on the underlying {@link HttpURLConnection}. + * + * @param request The request payload, if any, or null. + * @param responseProcessor A processor for the HTTP response. + */ + public <T> T execute(@Nullable byte[] request, @Nonnull ResponseProcessor<T> responseProcessor) + throws IOException, AuthException, HttpException { + if (DEBUG) { + Log.d(TAG, "execute: " + mConnection.getURL()); + } + try { + if (request != null) { + if (DEBUG) { + Log.d(TAG, "request size: " + request.length); + } + OutputStream out = new BufferedOutputStream(mConnection.getOutputStream()); + out.write(request); + out.flush(); + out.close(); + } + + final int responseCode = mConnection.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + Log.w(TAG, "Response error: " + responseCode + ", Message: " + + mConnection.getResponseMessage()); + if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + throw new AuthException(mConnection.getResponseMessage()); + } + throw new HttpException(responseCode); + } else { + if (DEBUG) { + Log.d(TAG, "request executed successfully"); + } + return responseProcessor.onSuccess(mConnection.getInputStream()); + } + } finally { + mConnection.disconnect(); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/network/HttpException.java b/java/src/com/android/inputmethod/latin/network/HttpException.java new file mode 100644 index 000000000..b9d8b6372 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/network/HttpException.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2014 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.network; + +import com.android.inputmethod.annotations.UsedForTesting; + +/** + * The HttpException exception represents a XML/HTTP fault with a HTTP status code. + */ +public class HttpException extends Exception { + + /** + * The HTTP status code. + */ + private final int mStatusCode; + + /** + * @param statusCode int HTTP status code. + */ + public HttpException(int statusCode) { + super("Response Code: " + statusCode); + mStatusCode = statusCode; + } + + /** + * @return the HTTP status code related to this exception. + */ + @UsedForTesting + public int getHttpStatusCode() { + return mStatusCode; + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/network/HttpUrlConnectionBuilder.java b/java/src/com/android/inputmethod/latin/network/HttpUrlConnectionBuilder.java new file mode 100644 index 000000000..502f72f17 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/network/HttpUrlConnectionBuilder.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2014 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.network; + +import android.text.TextUtils; + +import com.android.inputmethod.annotations.UsedForTesting; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map.Entry; + +/** + * Builder for {@link HttpURLConnection}s. + * + * TODO: Remove @UsedForTesting after this is actually used. + */ +@UsedForTesting +public class HttpUrlConnectionBuilder { + private static final int DEFAULT_TIMEOUT_MILLIS = 5 * 1000; + + /** + * Request header key for authentication. + */ + public static final String HTTP_HEADER_AUTHORIZATION = "Authorization"; + + /** + * Request header key for cache control. + */ + public static final String KEY_CACHE_CONTROL = "Cache-Control"; + /** + * Request header value for cache control indicating no caching. + * @see #KEY_CACHE_CONTROL + */ + public static final String VALUE_NO_CACHE = "no-cache"; + + /** + * Indicates that the request is unidirectional - upload-only. + * TODO: Remove @UsedForTesting after this is actually used. + */ + @UsedForTesting + public static final int MODE_UPLOAD_ONLY = 1; + /** + * Indicates that the request is unidirectional - download only. + * TODO: Remove @UsedForTesting after this is actually used. + */ + @UsedForTesting + public static final int MODE_DOWNLOAD_ONLY = 2; + /** + * Indicates that the request is bi-directional. + * TODO: Remove @UsedForTesting after this is actually used. + */ + @UsedForTesting + public static final int MODE_BI_DIRECTIONAL = 3; + + private final HashMap<String, String> mHeaderMap = new HashMap<>(); + + private URL mUrl; + private int mConnectTimeoutMillis = DEFAULT_TIMEOUT_MILLIS; + private int mReadTimeoutMillis = DEFAULT_TIMEOUT_MILLIS; + private int mContentLength = -1; + private boolean mUseCache; + private int mMode; + + /** + * Sets the URL that'll be used for the request. + * This *must* be set before calling {@link #build()} + * + * TODO: Remove @UsedForTesting after this method is actually used. + */ + @UsedForTesting + public HttpUrlConnectionBuilder setUrl(String url) throws MalformedURLException { + if (TextUtils.isEmpty(url)) { + throw new IllegalArgumentException("URL must not be empty"); + } + mUrl = new URL(url); + return this; + } + + /** + * Sets the connect timeout. Defaults to {@value #DEFAULT_TIMEOUT} milliseconds. + * + * TODO: Remove @UsedForTesting after this method is actually used. + */ + @UsedForTesting + public HttpUrlConnectionBuilder setConnectTimeout(int timeoutMillis) { + if (timeoutMillis < 0) { + throw new IllegalArgumentException("connect-timeout must be >= 0, but was " + + timeoutMillis); + } + mConnectTimeoutMillis = timeoutMillis; + return this; + } + + /** + * Sets the read timeout. Defaults to {@value #DEFAULT_TIMEOUT} milliseconds. + * + * TODO: Remove @UsedForTesting after this method is actually used. + */ + @UsedForTesting + public HttpUrlConnectionBuilder setReadTimeout(int timeoutMillis) { + if (timeoutMillis < 0) { + throw new IllegalArgumentException("read-timeout must be >= 0, but was " + + timeoutMillis); + } + mReadTimeoutMillis = timeoutMillis; + return this; + } + + /** + * Adds an entry to the request header. + * + * TODO: Remove @UsedForTesting after this method is actually used. + */ + @UsedForTesting + public HttpUrlConnectionBuilder addHeader(String key, String value) { + mHeaderMap.put(key, value); + return this; + } + + /** + * Sets an authentication token. + * + * TODO: Remove @UsedForTesting after this method is actually used. + */ + @UsedForTesting + public HttpUrlConnectionBuilder setAuthToken(String value) { + mHeaderMap.put(HTTP_HEADER_AUTHORIZATION, value); + return this; + } + + /** + * Sets the request to be executed such that the input is not buffered. + * This may be set when the request size is known beforehand. + * + * TODO: Remove @UsedForTesting after this method is actually used. + */ + @UsedForTesting + public HttpUrlConnectionBuilder setFixedLengthForStreaming(int length) { + mContentLength = length; + return this; + } + + /** + * Indicates if the request can use cached responses or not. + * + * TODO: Remove @UsedForTesting after this method is actually used. + */ + @UsedForTesting + public HttpUrlConnectionBuilder setUseCache(boolean useCache) { + mUseCache = useCache; + return this; + } + + /** + * The request mode. + * Sets the request mode to be one of: upload-only, download-only or bidirectional. + * + * @see #MODE_UPLOAD_ONLY + * @see #MODE_DOWNLOAD_ONLY + * @see #MODE_BI_DIRECTIONAL + * + * TODO: Remove @UsedForTesting after this method is actually used + */ + @UsedForTesting + public HttpUrlConnectionBuilder setMode(int mode) { + if (mode != MODE_UPLOAD_ONLY + && mode != MODE_DOWNLOAD_ONLY + && mode != MODE_BI_DIRECTIONAL) { + throw new IllegalArgumentException("Invalid mode specified:" + mode); + } + mMode = mode; + return this; + } + + /** + * Builds the {@link HttpURLConnection} instance that can be used to execute the request. + * + * TODO: Remove @UsedForTesting after this method is actually used. + */ + @UsedForTesting + public HttpURLConnection build() throws IOException { + if (mUrl == null) { + throw new IllegalArgumentException("A URL must be specified!"); + } + final HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); + connection.setConnectTimeout(mConnectTimeoutMillis); + connection.setReadTimeout(mReadTimeoutMillis); + connection.setUseCaches(mUseCache); + switch (mMode) { + case MODE_UPLOAD_ONLY: + connection.setDoInput(true); + connection.setDoOutput(false); + break; + case MODE_DOWNLOAD_ONLY: + connection.setDoInput(false); + connection.setDoOutput(true); + break; + case MODE_BI_DIRECTIONAL: + connection.setDoInput(true); + connection.setDoOutput(true); + break; + } + for (final Entry<String, String> entry : mHeaderMap.entrySet()) { + connection.addRequestProperty(entry.getKey(), entry.getValue()); + } + if (mContentLength >= 0) { + connection.setFixedLengthStreamingMode(mContentLength); + } + return connection; + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java index 9d72de8c5..734ed5583 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java @@ -18,20 +18,22 @@ package com.android.inputmethod.latin.personalization; import java.util.Collections; import java.util.List; -import java.util.Locale; public class PersonalizationDataChunk { + public static final String LANGUAGE_UNKNOWN = ""; + public final boolean mInputByUser; public final List<String> mTokens; public final int mTimestampInSeconds; public final String mPackageName; - public final Locale mlocale = null; + public final String mDetectedLanguage; public PersonalizationDataChunk(boolean inputByUser, final List<String> tokens, - final int timestampInSeconds, final String packageName) { + final int timestampInSeconds, final String packageName, final String detectedLanguage) { mInputByUser = inputByUser; mTokens = Collections.unmodifiableList(tokens); mTimestampInSeconds = timestampInSeconds; mPackageName = packageName; + mDetectedLanguage = detectedLanguage; } } diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java index 34d4d4ed7..59761547d 100644 --- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java +++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java @@ -23,7 +23,7 @@ import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.ExpandableBinaryDictionary; -import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.NgramContext; import com.android.inputmethod.latin.utils.DistracterFilter; import java.io.File; @@ -35,6 +35,7 @@ import java.util.Locale; */ public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBase { /* package */ static final String NAME = UserHistoryDictionary.class.getSimpleName(); + private final static int SUPPORTED_NGRAM = 2; // TODO: 3 // TODO: Make this constructor private /* package */ UserHistoryDictionary(final Context context, final Locale locale) { @@ -52,37 +53,19 @@ public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBas * Add a word to the user history dictionary. * * @param userHistoryDictionary the user history dictionary - * @param prevWordsInfo the information of previous words + * @param ngramContext the n-gram context * @param word the word the user inputted * @param isValid whether the word is valid or not * @param timestamp the timestamp when the word has been inputted * @param distracterFilter the filter to check whether the word is a distracter */ public static void addToDictionary(final ExpandableBinaryDictionary userHistoryDictionary, - final PrevWordsInfo prevWordsInfo, final String word, final boolean isValid, + final NgramContext ngramContext, final String word, final boolean isValid, final int timestamp, final DistracterFilter distracterFilter) { - final CharSequence prevWord = prevWordsInfo.mPrevWordsInfo[0].mWord; - if (word.length() > Constants.DICTIONARY_MAX_WORD_LENGTH || - (prevWord != null && prevWord.length() > Constants.DICTIONARY_MAX_WORD_LENGTH)) { + if (word.length() > Constants.DICTIONARY_MAX_WORD_LENGTH) { return; } - final int frequency = isValid ? - FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS; - userHistoryDictionary.addUnigramEntryWithCheckingDistracter(word, frequency, - null /* shortcutTarget */, 0 /* shortcutFreq */, false /* isNotAWord */, - false /* isBlacklisted */, timestamp, distracterFilter); - // Do not insert a word as a bigram of itself - if (TextUtils.equals(word, prevWord)) { - return; - } - if (null != prevWord) { - if (prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence) { - // Beginning-of-Sentence n-gram entry is treated as a n-gram entry of invalid word. - userHistoryDictionary.addNgramEntry(prevWordsInfo, word, - FREQUENCY_FOR_WORDS_NOT_IN_DICTS, timestamp); - } else { - userHistoryDictionary.addNgramEntry(prevWordsInfo, word, frequency, timestamp); - } - } + userHistoryDictionary.updateEntriesForWordWithCheckingDistracter(ngramContext, word, + isValid, 1 /* count */, timestamp, distracterFilter); } } diff --git a/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java new file mode 100644 index 000000000..4bd15d037 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2014 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.settings; + +import static com.android.inputmethod.latin.settings.LocalSettingsConstants.PREF_ACCOUNT_NAME; +import static com.android.inputmethod.latin.settings.LocalSettingsConstants.PREF_ENABLE_CLOUD_SYNC; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; +import android.text.TextUtils; +import android.widget.ListView; +import android.widget.Toast; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.SubtypeSwitcher; +import com.android.inputmethod.latin.accounts.LoginAccountUtils; +import com.android.inputmethod.latin.accounts.AccountStateChangedListener; +import com.android.inputmethod.latin.define.ProductionFlags; + +import javax.annotation.Nullable; + +/** + * "Accounts & Privacy" settings sub screen. + * + * This settings sub screen handles the following preferences: + * <li> Account selection/management for IME </li> + * <li> Sync preferences </li> + * <li> Privacy preferences </li> + */ +public final class AccountsSettingsFragment extends SubScreenFragment { + private static final String PREF_SYNC_NOW = "pref_beanstalk"; + + static final String PREF_ACCCOUNT_SWITCHER = "account_switcher"; + + private final DialogInterface.OnClickListener mAccountChangedListener = + new AccountChangedListener(); + private final Preference.OnPreferenceClickListener mSyncNowListener = new SyncNowListener(); + + @Override + public void onCreate(final Bundle icicle) { + super.onCreate(icicle); + addPreferencesFromResource(R.xml.prefs_screen_accounts); + + final Resources res = getResources(); + final Context context = getActivity(); + + // When we are called from the Settings application but we are not already running, some + // singleton and utility classes may not have been initialized. We have to call + // initialization method of these classes here. See {@link LatinIME#onCreate()}. + SubtypeSwitcher.init(context); + + if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) { + final Preference enableMetricsLogging = + findPreference(Settings.PREF_ENABLE_METRICS_LOGGING); + if (enableMetricsLogging != null) { + final String enableMetricsLoggingTitle = res.getString( + R.string.enable_metrics_logging, getApplicationName()); + enableMetricsLogging.setTitle(enableMetricsLoggingTitle); + } + } else { + removePreference(Settings.PREF_ENABLE_METRICS_LOGGING); + } + + if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) { + removePreference(PREF_ACCCOUNT_SWITCHER); + removePreference(PREF_ENABLE_CLOUD_SYNC); + removePreference(PREF_SYNC_NOW); + } + if (!ProductionFlags.ENABLE_PERSONAL_DICTIONARY_SYNC) { + removePreference(PREF_ENABLE_CLOUD_SYNC); + removePreference(PREF_SYNC_NOW); + } else { + final Preference syncNowPreference = findPreference(PREF_SYNC_NOW); + syncNowPreference.setOnPreferenceClickListener(mSyncNowListener); + } + } + + @Override + public void onResume() { + super.onResume(); + refreshAccountAndDependentPreferences(getSignedInAccountName()); + } + + @Override + public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { + if (TextUtils.equals(key, PREF_ACCOUNT_NAME)) { + refreshAccountAndDependentPreferences( + prefs.getString(PREF_ACCOUNT_NAME, null)); + } else if (TextUtils.equals(key, PREF_ENABLE_CLOUD_SYNC)) { + final boolean syncEnabled = prefs.getBoolean(PREF_ENABLE_CLOUD_SYNC, false); + AccountStateChangedListener.onSyncPreferenceChanged( + getSignedInAccountName(), syncEnabled); + } + } + + private void refreshAccountAndDependentPreferences(@Nullable final String currentAccount) { + if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) { + return; + } + + final Preference accountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER); + if (currentAccount == null) { + // No account is currently selected. + accountSwitcher.setSummary(getString(R.string.no_accounts_selected)); + // Disable the sync preference UI. + disableSyncPreference(); + } else { + // Set the currently selected account. + accountSwitcher.setSummary(getString(R.string.account_selected, currentAccount)); + // Enable the sync preference UI. + enableSyncPreference(); + } + // Set up onClick listener for the account picker preference. + final Context context = getActivity(); + final String[] accountsForLogin = LoginAccountUtils.getAccountsForLogin(context); + accountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + if (accountsForLogin.length == 0) { + // TODO: Handle account addition. + Toast.makeText(getActivity(), getString(R.string.account_select_cancel), + Toast.LENGTH_SHORT).show(); + } else { + createAccountPicker(accountsForLogin, currentAccount).show(); + } + return true; + } + }); + } + + /** + * Enables the Sync preference UI and updates its summary. + */ + private void enableSyncPreference() { + if (!ProductionFlags.ENABLE_PERSONAL_DICTIONARY_SYNC) { + return; + } + + final Preference syncPreference = findPreference(PREF_ENABLE_CLOUD_SYNC); + syncPreference.setEnabled(true); + syncPreference.setSummary(R.string.cloud_sync_summary); + } + + /** + * Disables the Sync preference UI and updates its summary to indicate + * the fact that an account needs to be selected for sync. + */ + private void disableSyncPreference() { + if (!ProductionFlags.ENABLE_PERSONAL_DICTIONARY_SYNC) { + return; + } + + final Preference syncPreference = findPreference(PREF_ENABLE_CLOUD_SYNC); + syncPreference.setEnabled(false); + syncPreference.setSummary(R.string.cloud_sync_summary_disabled_signed_out); + } + + @Nullable + String getSignedInAccountName() { + return getSharedPreferences().getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null); + } + + boolean isSyncEnabled() { + return getSharedPreferences().getBoolean(PREF_ENABLE_CLOUD_SYNC, false); + } + + /** + * Creates an account picker dialog showing the given accounts in a list and selecting + * the selected account by default. + * The list of accounts must not be null/empty. + * + * Package-private for testing. + */ + @UsedForTesting + AlertDialog createAccountPicker(final String[] accounts, + final String selectedAccount) { + if (accounts == null || accounts.length == 0) { + throw new IllegalArgumentException("List of accounts must not be empty"); + } + + // See if the currently selected account is in the list. + // If it is, the entry is selected, and a sign-out button is provided. + // If it isn't, select the 0th account by default which will get picked up + // if the user presses OK. + int index = 0; + boolean isSignedIn = false; + for (int i = 0; i < accounts.length; i++) { + if (TextUtils.equals(accounts[i], selectedAccount)) { + index = i; + isSignedIn = true; + break; + } + } + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) + .setTitle(R.string.account_select_title) + .setSingleChoiceItems(accounts, index, null) + .setPositiveButton(R.string.account_select_ok, mAccountChangedListener) + .setNegativeButton(R.string.account_select_cancel, null); + if (isSignedIn) { + builder.setNeutralButton(R.string.account_select_sign_out, mAccountChangedListener); + } + return builder.create(); + } + + /** + * Listener for a account selection changes from the picker. + * Persists/removes the account to/from shared preferences and sets up sync if required. + */ + class AccountChangedListener implements DialogInterface.OnClickListener { + @Override + public void onClick(DialogInterface dialog, int which) { + final String oldAccount = getSignedInAccountName(); + switch (which) { + case DialogInterface.BUTTON_POSITIVE: // Signed in + final ListView lv = ((AlertDialog)dialog).getListView(); + final String newAccount = + (String) lv.getItemAtPosition(lv.getCheckedItemPosition()); + getSharedPreferences() + .edit() + .putString(PREF_ACCOUNT_NAME, newAccount) + .apply(); + AccountStateChangedListener.onAccountSignedIn(oldAccount, newAccount); + break; + case DialogInterface.BUTTON_NEUTRAL: // Signed out + AccountStateChangedListener.onAccountSignedOut(oldAccount); + getSharedPreferences() + .edit() + .remove(PREF_ACCOUNT_NAME) + .apply(); + break; + } + } + } + + /** + * Listener that initiates the process of sync in the background. + */ + class SyncNowListener implements Preference.OnPreferenceClickListener { + @Override + public boolean onPreferenceClick(final Preference preference) { + AccountStateChangedListener.forceSync(getSignedInAccountName()); + return true; + } + } +} diff --git a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java index 00f2c73dd..3303ab093 100644 --- a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java @@ -93,20 +93,24 @@ public final class AdvancedSettingsFragment extends SubScreenFragment { removePreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON); } + // If metrics logging isn't supported, or account sign in is enabled + // don't show the logging preference. + // TODO: Eventually when we enable account sign in by default, + // we'll remove logging preference from here. if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) { final Preference enableMetricsLogging = findPreference(Settings.PREF_ENABLE_METRICS_LOGGING); if (enableMetricsLogging != null) { - final int applicationLabelRes = context.getApplicationInfo().labelRes; - final String applicationName = res.getString(applicationLabelRes); final String enableMetricsLoggingTitle = res.getString( - R.string.enable_metrics_logging, applicationName); + R.string.enable_metrics_logging, getApplicationName()); enableMetricsLogging.setTitle(enableMetricsLoggingTitle); } } else { removePreference(Settings.PREF_ENABLE_METRICS_LOGGING); } + AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(context, this); + setupKeypressVibrationDurationSettings(); setupKeypressSoundVolumeSettings(); refreshEnablingsOfKeypressSoundAndVibrationSettings(); diff --git a/java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java index f5e4d33a2..a9884ba13 100644 --- a/java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/AppearanceSettingsFragment.java @@ -19,6 +19,7 @@ package com.android.inputmethod.latin.settings; import android.os.Bundle; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.define.ProductionFlags; /** @@ -29,6 +30,10 @@ public final class AppearanceSettingsFragment extends SubScreenFragment { public void onCreate(final Bundle icicle) { super.onCreate(icicle); addPreferencesFromResource(R.xml.prefs_screen_appearance); + if (!ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED + || !Settings.getInstance().getCurrent().isTablet()) { + removePreference(Settings.PREF_ENABLE_SPLIT_KEYBOARD); + } } @Override @@ -38,4 +43,4 @@ public final class AppearanceSettingsFragment extends SubScreenFragment { findPreference(Settings.PREF_CUSTOM_INPUT_STYLES)); ThemeSettingsFragment.updateKeyboardThemeSummary(findPreference(Settings.SCREEN_THEME)); } -} +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java index 9bc398654..c633fc167 100644 --- a/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/CustomInputStyleSettingsFragment.java @@ -32,7 +32,7 @@ import android.preference.PreferenceFragment; import android.preference.PreferenceGroup; import android.support.v4.view.ViewCompat; import android.text.TextUtils; -import android.util.Pair; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -59,6 +59,12 @@ import java.util.ArrayList; import java.util.TreeSet; public final class CustomInputStyleSettingsFragment extends PreferenceFragment { + private static final String TAG = CustomInputStyleSettingsFragment.class.getSimpleName(); + private static final boolean DEBUG_SUBTYPE_ID = false; + // Note: We would like to turn this debug flag true in order to see what input styles are + // defined in a bug-report. + private static final boolean DEBUG_CUSTOM_INPUT_STYLES = true; + private RichInputMethodManager mRichImm; private SharedPreferences mPrefs; private SubtypeLocaleAdapter mSubtypeLocaleAdapter; @@ -73,31 +79,31 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { "is_subtype_enabler_notification_dialog_open"; private static final String KEY_SUBTYPE_FOR_SUBTYPE_ENABLER = "subtype_for_subtype_enabler"; - static final class SubtypeLocaleItem extends Pair<String, String> - implements Comparable<SubtypeLocaleItem> { - public SubtypeLocaleItem(final String localeString, final String displayName) { - super(localeString, displayName); - } + static final class SubtypeLocaleItem implements Comparable<SubtypeLocaleItem> { + public final String mLocaleString; + private final String mDisplayName; - public SubtypeLocaleItem(final String localeString) { - this(localeString, - SubtypeLocaleUtils.getSubtypeLocaleDisplayNameInSystemLocale(localeString)); + public SubtypeLocaleItem(final InputMethodSubtype subtype) { + mLocaleString = subtype.getLocale(); + mDisplayName = SubtypeLocaleUtils.getSubtypeLocaleDisplayNameInSystemLocale( + mLocaleString); } + // {@link ArrayAdapter<T>} that hosts the instance of this class needs {@link #toString()} + // to get display name. @Override public String toString() { - return second; + return mDisplayName; } @Override public int compareTo(final SubtypeLocaleItem o) { - return first.compareTo(o.first); + return mLocaleString.compareTo(o.mLocaleString); } } static final class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> { - private static final String TAG = SubtypeLocaleAdapter.class.getSimpleName(); - private static final boolean DEBUG_SUBTYPE_ID = false; + private static final String TAG_SUBTYPE = SubtypeLocaleAdapter.class.getSimpleName(); public SubtypeLocaleAdapter(final Context context) { super(context, android.R.layout.simple_spinner_item); @@ -110,37 +116,33 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { for (int i = 0; i < count; i++) { final InputMethodSubtype subtype = imi.getSubtypeAt(i); if (DEBUG_SUBTYPE_ID) { - android.util.Log.d(TAG, String.format("%-6s 0x%08x %11d %s", + Log.d(TAG_SUBTYPE, String.format("%-6s 0x%08x %11d %s", subtype.getLocale(), subtype.hashCode(), subtype.hashCode(), SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype))); } if (InputMethodSubtypeCompatUtils.isAsciiCapable(subtype)) { - items.add(createItem(context, subtype.getLocale())); + items.add(new SubtypeLocaleItem(subtype)); } } // TODO: Should filter out already existing combinations of locale and layout. addAll(items); } - - public static SubtypeLocaleItem createItem(final Context context, - final String localeString) { - if (localeString.equals(SubtypeLocaleUtils.NO_LANGUAGE)) { - final String displayName = context.getString(R.string.subtype_no_language); - return new SubtypeLocaleItem(localeString, displayName); - } - return new SubtypeLocaleItem(localeString); - } } - static final class KeyboardLayoutSetItem extends Pair<String, String> { + static final class KeyboardLayoutSetItem { + public final String mLayoutName; + private final String mDisplayName; + public KeyboardLayoutSetItem(final InputMethodSubtype subtype) { - super(SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype), - SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype)); + mLayoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype); + mDisplayName = SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype); } + // {@link ArrayAdapter<T>} that hosts the instance of this class needs {@link #toString()} + // to get display name. @Override public String toString() { - return second; + return mDisplayName; } } @@ -249,7 +251,6 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { @Override protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) { - final Context context = builder.getContext(); builder.setCancelable(true).setOnCancelListener(this); if (isIncomplete()) { builder.setPositiveButton(R.string.add, this) @@ -258,8 +259,7 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { builder.setPositiveButton(R.string.save, this) .setNeutralButton(android.R.string.cancel, this) .setNegativeButton(R.string.remove, this); - final SubtypeLocaleItem localeItem = SubtypeLocaleAdapter.createItem( - context, mSubtype.getLocale()); + final SubtypeLocaleItem localeItem = new SubtypeLocaleItem(mSubtype); final KeyboardLayoutSetItem layoutItem = new KeyboardLayoutSetItem(mSubtype); setSpinnerPosition(mSubtypeLocaleSpinner, localeItem); setSpinnerPosition(mKeyboardLayoutSetSpinner, layoutItem); @@ -297,7 +297,7 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { (KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem(); final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( - locale.first, layout.first); + locale.mLocaleString, layout.mLayoutName); setSubtype(subtype); notifyChanged(); if (isEditing) { @@ -445,6 +445,9 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { final String prefSubtypes = Settings.readPrefAdditionalSubtypes(mPrefs, getResources()); + if (DEBUG_CUSTOM_INPUT_STYLES) { + Log.i(TAG, "Load custom input styles: " + prefSubtypes); + } setPrefSubtypes(prefSubtypes, context); mIsAddingNewSubtype = (savedInstanceState != null) @@ -460,8 +463,6 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN)) { mSubtypePreferenceKeyForSubtypeEnabler = savedInstanceState.getString( KEY_SUBTYPE_FOR_SUBTYPE_ENABLER); - final SubtypePreference subtypePref = (SubtypePreference)findPreference( - mSubtypePreferenceKeyForSubtypeEnabler); mSubtypeEnablerNotificationDialog = createDialog(); mSubtypeEnablerNotificationDialog.show(); } @@ -611,6 +612,9 @@ public final class CustomInputStyleSettingsFragment extends PreferenceFragment { final String oldSubtypes = Settings.readPrefAdditionalSubtypes(mPrefs, getResources()); final InputMethodSubtype[] subtypes = getSubtypes(); final String prefSubtypes = AdditionalSubtypeUtils.createPrefSubtypes(subtypes); + if (DEBUG_CUSTOM_INPUT_STYLES) { + Log.i(TAG, "Save custom input styles: " + prefSubtypes); + } if (prefSubtypes.equals(oldSubtypes)) { return; } diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java index 48f4c758c..df0378e1d 100644 --- a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java +++ b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java @@ -16,29 +16,37 @@ package com.android.inputmethod.latin.settings; +/** + * Debug settings for the application. + * + * Note: Even though these settings are stored in the default shared preferences file, + * they shouldn't be restored across devices. + * If a new key is added here, it should also be blacklisted for restore in + * {@link LocalSettingsConstants}. + */ public final class DebugSettings { public static final String PREF_DEBUG_MODE = "debug_mode"; public static final String PREF_FORCE_NON_DISTINCT_MULTITOUCH = "force_non_distinct_multitouch"; public static final String PREF_FORCE_PHYSICAL_KEYBOARD_SPECIAL_KEY = "force_physical_keyboard_special_key"; - public static final String PREF_SHOW_UI_TO_ACCEPT_TYPED_WORD = - "pref_show_ui_to_accept_typed_word"; public static final String PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS = "pref_has_custom_key_preview_animation_params"; - public static final String PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE = - "pref_key_preview_show_up_start_x_scale"; - public static final String PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE = - "pref_key_preview_show_up_start_y_scale"; + public static final String PREF_KEY_LONGPRESS_TIMEOUT = "pref_key_longpress_timeout"; + public static final String PREF_KEY_PREVIEW_DISMISS_DURATION = + "pref_key_preview_dismiss_duration"; public static final String PREF_KEY_PREVIEW_DISMISS_END_X_SCALE = "pref_key_preview_dismiss_end_x_scale"; public static final String PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE = "pref_key_preview_dismiss_end_y_scale"; public static final String PREF_KEY_PREVIEW_SHOW_UP_DURATION = "pref_key_preview_show_up_duration"; - public static final String PREF_KEY_PREVIEW_DISMISS_DURATION = - "pref_key_preview_dismiss_duration"; + public static final String PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE = + "pref_key_preview_show_up_start_x_scale"; + public static final String PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE = + "pref_key_preview_show_up_start_y_scale"; + public static final String PREF_SHOULD_SHOW_LXX_SUGGESTION_UI = + "pref_should_show_lxx_suggestion_ui"; public static final String PREF_SLIDING_KEY_INPUT_PREVIEW = "pref_sliding_key_input_preview"; - public static final String PREF_KEY_LONGPRESS_TIMEOUT = "pref_key_longpress_timeout"; private DebugSettings() { // This class is not publicly instantiable. diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java index 5640e2039..e9f8d45aa 100644 --- a/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/DebugSettingsFragment.java @@ -56,8 +56,8 @@ public final class DebugSettingsFragment extends SubScreenFragment super.onCreate(icicle); addPreferencesFromResource(R.xml.prefs_screen_debug); - if (!Settings.HAS_UI_TO_ACCEPT_TYPED_WORD) { - removePreference(DebugSettings.PREF_SHOW_UI_TO_ACCEPT_TYPED_WORD); + if (!Settings.SHOULD_SHOW_LXX_SUGGESTION_UI) { + removePreference(DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI); } mReadExternalDictionaryPref = findPreference(PREF_READ_EXTERNAL_DICTIONARY); diff --git a/java/src/com/android/inputmethod/latin/settings/LocalSettingsConstants.java b/java/src/com/android/inputmethod/latin/settings/LocalSettingsConstants.java new file mode 100644 index 000000000..c17110471 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/settings/LocalSettingsConstants.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 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.settings; + +/** + * Collection of device specific preference constants. + */ +public class LocalSettingsConstants { + // Preference file for storing preferences that are tied to a device + // and are not backed up. + public static final String PREFS_FILE = "local_prefs"; + + // Preference key for the current account. + // Do not restore. + public static final String PREF_ACCOUNT_NAME = "pref_account_name"; + // Preference key for enabling cloud sync feature. + // Do not restore. + public static final String PREF_ENABLE_CLOUD_SYNC = "pref_enable_cloud_sync"; + + // List of preference keys to skip from being restored by backup agent. + // These preferences are tied to a device and hence should not be restored. + // e.g. account name. + // Ideally they could have been kept in a separate file that wasn't backed up + // however the preference UI currently only deals with the default + // shared preferences which makes it non-trivial to move these out to + // a different shared preferences file. + public static final String[] PREFS_TO_SKIP_RESTORING = new String[] { + PREF_ACCOUNT_NAME, + PREF_ENABLE_CLOUD_SYNC, + // The debug settings are not restored on a new device. + // If a feature relies on these, it should ensure that the defaults are + // correctly set for it to work on a new device. + DebugSettings.PREF_DEBUG_MODE, + DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH, + DebugSettings.PREF_FORCE_PHYSICAL_KEYBOARD_SPECIAL_KEY, + DebugSettings.PREF_HAS_CUSTOM_KEY_PREVIEW_ANIMATION_PARAMS, + DebugSettings.PREF_KEY_LONGPRESS_TIMEOUT, + DebugSettings.PREF_KEY_PREVIEW_DISMISS_DURATION, + DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_X_SCALE, + DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_Y_SCALE, + DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION, + DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_X_SCALE, + DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_Y_SCALE, + DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI, + DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW + }; +} diff --git a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java index 31a20c4db..7603dbba5 100644 --- a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java +++ b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java @@ -22,7 +22,8 @@ public class NativeSuggestOptions { private static final int USE_FULL_EDIT_DISTANCE = 1; private static final int BLOCK_OFFENSIVE_WORDS = 2; private static final int SPACE_AWARE_GESTURE_ENABLED = 3; - private static final int OPTIONS_SIZE = 4; + private static final int WEIGHT_FOR_LOCALE_IN_THOUSANDS = 4; + private static final int OPTIONS_SIZE = 5; private final int[] mOptions = new int[OPTIONS_SIZE + AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE]; @@ -43,6 +44,12 @@ public class NativeSuggestOptions { setBooleanOption(SPACE_AWARE_GESTURE_ENABLED, value); } + public void setWeightForLocale(final float value) { + // We're passing this option as a fixed point value, in thousands. This is decoded in + // native code by SuggestOptions#weightForLocale(). + setIntegerOption(WEIGHT_FOR_LOCALE_IN_THOUSANDS, (int) (value * 1000)); + } + public void setAdditionalFeaturesOptions(final int[] additionalOptions) { if (additionalOptions == null) { return; diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index 0de2d8831..103033c16 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -32,6 +32,7 @@ import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; import com.android.inputmethod.latin.utils.ResourceUtils; import com.android.inputmethod.latin.utils.RunInLocale; +import com.android.inputmethod.latin.utils.StatsUtils; import com.android.inputmethod.latin.utils.StringUtils; import java.util.Collections; @@ -43,6 +44,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang private static final String TAG = Settings.class.getSimpleName(); // Settings screens public static final String SCREEN_PREFERENCES = "screen_preferences"; + public static final String SCREEN_ACCOUNTS = "screen_accounts"; public static final String SCREEN_APPEARANCE = "screen_appearance"; public static final String SCREEN_THEME = "screen_theme"; public static final String SCREEN_MULTILINGUAL = "screen_multilingual"; @@ -75,7 +77,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.KITKAT; public static final boolean ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS = BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.KITKAT; - public static final boolean HAS_UI_TO_ACCEPT_TYPED_WORD = + public static final boolean SHOULD_SHOW_LXX_SUGGESTION_UI = BuildCompatUtils.EFFECTIVE_SDK_INT >= BuildCompatUtils.VERSION_CODES_LXX; public static final String PREF_SHOW_LANGUAGE_SWITCH_KEY = "pref_show_language_switch_key"; @@ -83,6 +85,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang "pref_include_other_imes_in_language_switch_list"; public static final String PREF_KEYBOARD_THEME = "pref_keyboard_theme"; public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles"; + public static final String PREF_ENABLE_SPLIT_KEYBOARD = "pref_split_keyboard"; // TODO: consolidate key preview dismiss delay with the key preview animation parameters. public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY = "pref_key_preview_popup_dismiss_delay"; @@ -103,7 +106,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_KEY_IS_INTERNAL = "pref_key_is_internal"; public static final String PREF_ENABLE_METRICS_LOGGING = "pref_enable_metrics_logging"; - // This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead. // This is being used only for the backward compatibility. private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY = @@ -166,6 +168,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return; } loadSettings(mContext, mSettingsValues.mLocale, mSettingsValues.mInputAttributes); + StatsUtils.onLoadSettings(mSettingsValues); } finally { mSettingsValuesLock.unlock(); } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java index 4fc17387f..8c4801798 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java @@ -25,6 +25,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.define.ProductionFlags; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.FeedbackUtils; import com.android.inputmethodcommon.InputMethodSettingsFragment; @@ -51,6 +52,10 @@ public final class SettingsFragment extends InputMethodSettingsFragment { final Preference multilingualOptions = findPreference(Settings.SCREEN_MULTILINGUAL); preferenceScreen.removePreference(multilingualOptions); } + if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) { + final Preference accountsPreference = findPreference(Settings.SCREEN_ACCOUNTS); + preferenceScreen.removePreference(accountsPreference); + } } @Override diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index d8c548d8b..660b4e095 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -25,6 +25,7 @@ import android.util.Log; import android.view.inputmethod.EditorInfo; import com.android.inputmethod.compat.AppWorkaroundsUtils; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.InputAttributes; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; @@ -40,7 +41,8 @@ import java.util.Locale; * When you call the constructor of this class, you may want to change the current system locale by * using {@link com.android.inputmethod.latin.utils.RunInLocale}. */ -public final class SettingsValues { +// Non-final for testing via mock library. +public class SettingsValues { private static final String TAG = SettingsValues.class.getSimpleName(); // "floatMaxValue" and "floatNegativeInfinity" are special marker strings for // Float.NEGATIVE_INFINITE and Float.MAX_VALUE. Currently used for auto-correction settings. @@ -77,7 +79,10 @@ public final class SettingsValues { public final boolean mPhraseGestureEnabled; public final int mKeyLongpressTimeout; public final boolean mEnableMetricsLogging; - public final boolean mShouldShowUiToAcceptTypedWord; + public final boolean mShouldShowLxxSuggestionUi; + // Use split layout for keyboard. + public final boolean mIsSplitKeyboardEnabled; + public final int mScreenMetrics; // From the input box public final InputAttributes mInputAttributes; @@ -153,8 +158,11 @@ public final class SettingsValues { 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); + mIsSplitKeyboardEnabled = prefs.getBoolean(Settings.PREF_ENABLE_SPLIT_KEYBOARD, false); + mScreenMetrics = res.getInteger(R.integer.config_screen_metrics); + + mShouldShowLxxSuggestionUi = Settings.SHOULD_SHOW_LXX_SUGGESTION_UI + && prefs.getBoolean(DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI, true); // Compute other readable settings mKeyLongpressTimeout = Settings.readKeyLongpressTimeout(prefs, res); mKeypressVibrationDuration = Settings.readKeypressVibrationDuration(prefs, res); @@ -164,13 +172,13 @@ public final class SettingsValues { autoCorrectionThresholdRawValue); mGestureInputEnabled = Settings.readGestureInputEnabled(prefs, res); mGestureTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true); - mGestureFloatingPreviewTextEnabled = prefs.getBoolean( - Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true); + mGestureFloatingPreviewTextEnabled = !mInputAttributes.mDisableGestureFloatingPreviewText + && prefs.getBoolean(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true); mPhraseGestureEnabled = Settings.readPhraseGestureEnabled(prefs, res); mAutoCorrectionEnabledPerUserSettings = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; mSuggestionsEnabledPerUserSettings = readSuggestionsEnabled(prefs); - AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray( + AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray(context, prefs, mAdditionalFeaturesSettingValues); mTextHighlightColorForAddToDictionaryIndicator = res.getColor( R.color.text_decorator_add_to_dictionary_indicator_text_highlight_color); @@ -211,6 +219,15 @@ public final class SettingsValues { } } + public boolean isMetricsLoggingEnabled() { + return mEnableMetricsLogging; + } + + public boolean isTablet() { + return mScreenMetrics == Constants.SCREEN_METRICS_SMALL_TABLET + || mScreenMetrics == Constants.SCREEN_METRICS_LARGE_TABLET; + } + public boolean isApplicationSpecifiedCompletionsOn() { return mInputAttributes.mApplicationSpecifiedCompletionOn; } diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java index 49d81104d..97aad3b6d 100644 --- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java +++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java @@ -36,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; @@ -55,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); @@ -77,8 +82,10 @@ public final class SpacingAndPunctuations { 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; @@ -109,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/settings/SubScreenFragment.java b/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java index ca5b395ce..240f8f89b 100644 --- a/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java @@ -20,6 +20,7 @@ import android.app.backup.BackupManager; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.content.res.Resources; import android.os.Bundle; import android.preference.ListPreference; import android.preference.Preference; @@ -79,6 +80,16 @@ abstract class SubScreenFragment extends PreferenceFragment return getPreferenceManager().getSharedPreferences(); } + /** + * Gets the application name to display on the UI. + */ + final String getApplicationName() { + final Context context = getActivity(); + final Resources res = getResources(); + final int applicationLabelRes = context.getApplicationInfo().labelRes; + return res.getString(applicationLabelRes); + } + @Override public void addPreferencesFromResource(final int preferencesResId) { super.addPreferencesFromResource(preferencesResId); diff --git a/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java b/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java new file mode 100644 index 000000000..254bc6567 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 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.settings; + +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.Intent; +import android.os.Bundle; + +/** + * Test activity to use when testing preference fragments. <br/> + * Usage: <br/> + * Create an ActivityInstrumentationTestCase2 for this activity + * and call setIntent() with an intent that specifies the fragment to load in the activity. + * The fragment can then be obtained from this activity and used for testing/verification. + */ +public final class TestFragmentActivity extends Activity { + /** + * The fragment name that should be loaded when starting this activity. + * This must be specified when starting this activity, as this activity is only + * meant to test fragments from instrumentation tests. + */ + public static final String EXTRA_SHOW_FRAGMENT = "show_fragment"; + + public Fragment mFragment; + + @Override + protected void onCreate(final Bundle savedState) { + super.onCreate(savedState); + final Intent intent = getIntent(); + final String fragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT); + if (fragmentName == null) { + throw new IllegalArgumentException("No fragment name specified for testing"); + } + + mFragment = Fragment.instantiate(this, fragmentName); + FragmentManager fragmentManager = getFragmentManager(); + fragmentManager.beginTransaction().add(mFragment, fragmentName).commit(); + } +} diff --git a/java/src/com/android/inputmethod/latin/settings/ThemeSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/ThemeSettingsFragment.java index 5a3fc3600..29289aed2 100644 --- a/java/src/com/android/inputmethod/latin/settings/ThemeSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/ThemeSettingsFragment.java @@ -17,7 +17,6 @@ package com.android.inputmethod.latin.settings; import android.content.Context; -import android.content.SharedPreferences; import android.content.res.Resources; import android.os.Bundle; import android.preference.Preference; @@ -32,12 +31,12 @@ import com.android.inputmethod.latin.settings.RadioButtonPreference.OnRadioButto */ public final class ThemeSettingsFragment extends SubScreenFragment implements OnRadioButtonClickedListener { - private String mSelectedThemeId; + private int mSelectedThemeId; static class KeyboardThemePreference extends RadioButtonPreference { - final String mThemeId; + final int mThemeId; - KeyboardThemePreference(final Context context, final String name, final String id) { + KeyboardThemePreference(final Context context, final String name, final int id) { super(context); setTitle(name); mThemeId = id; @@ -45,14 +44,13 @@ public final class ThemeSettingsFragment extends SubScreenFragment } static void updateKeyboardThemeSummary(final Preference pref) { - final Resources res = pref.getContext().getResources(); - final SharedPreferences prefs = pref.getSharedPreferences(); - final KeyboardTheme keyboardTheme = KeyboardTheme.getKeyboardTheme(prefs); - final String keyboardThemeId = String.valueOf(keyboardTheme.mThemeId); + final Context context = pref.getContext(); + final Resources res = context.getResources(); + final KeyboardTheme keyboardTheme = KeyboardTheme.getKeyboardTheme(context); final String[] keyboardThemeNames = res.getStringArray(R.array.keyboard_theme_names); - final String[] keyboardThemeIds = res.getStringArray(R.array.keyboard_theme_ids); + final int[] keyboardThemeIds = res.getIntArray(R.array.keyboard_theme_ids); for (int index = 0; index < keyboardThemeNames.length; index++) { - if (keyboardThemeId.equals(keyboardThemeIds[index])) { + if (keyboardTheme.mThemeId == keyboardThemeIds[index]) { pref.setSummary(keyboardThemeNames[index]); return; } @@ -64,18 +62,18 @@ public final class ThemeSettingsFragment extends SubScreenFragment super.onCreate(icicle); addPreferencesFromResource(R.xml.prefs_screen_theme); final PreferenceScreen screen = getPreferenceScreen(); + final Context context = getActivity(); final Resources res = getResources(); final String[] keyboardThemeNames = res.getStringArray(R.array.keyboard_theme_names); - final String[] keyboardThemeIds = res.getStringArray(R.array.keyboard_theme_ids); + final int[] keyboardThemeIds = res.getIntArray(R.array.keyboard_theme_ids); for (int index = 0; index < keyboardThemeNames.length; index++) { final KeyboardThemePreference pref = new KeyboardThemePreference( - getActivity(), keyboardThemeNames[index], keyboardThemeIds[index]); + context, keyboardThemeNames[index], keyboardThemeIds[index]); screen.addPreference(pref); pref.setOnRadioButtonClickedListener(this); } - final SharedPreferences prefs = getSharedPreferences(); - final KeyboardTheme keyboardTheme = KeyboardTheme.getKeyboardTheme(prefs); - mSelectedThemeId = String.valueOf(keyboardTheme.mThemeId); + final KeyboardTheme keyboardTheme = KeyboardTheme.getKeyboardTheme(context); + mSelectedThemeId = keyboardTheme.mThemeId; } @Override @@ -106,7 +104,7 @@ public final class ThemeSettingsFragment extends SubScreenFragment final Preference preference = screen.getPreference(index); if (preference instanceof KeyboardThemePreference) { final KeyboardThemePreference pref = (KeyboardThemePreference)preference; - final boolean selected = mSelectedThemeId.equals(pref.mThemeId); + final boolean selected = (mSelectedThemeId == pref.mThemeId); pref.setSelected(selected); } } diff --git a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java index b770ea512..7607429f8 100644 --- a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java +++ b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java @@ -17,12 +17,8 @@ package com.android.inputmethod.latin.setup; import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.provider.Settings; -import android.view.inputmethod.InputMethodInfo; -import android.view.inputmethod.InputMethodManager; public final class SetupActivity extends Activity { @Override diff --git a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java index e455e53d3..c3b30dcb4 100644 --- a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java +++ b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java @@ -42,10 +42,14 @@ import com.android.inputmethod.latin.utils.UncachedInputMethodManagerUtils; import java.util.ArrayList; +import javax.annotation.Nonnull; + // TODO: Use Fragment to implement welcome screen and setup steps. public final class SetupWizardActivity extends Activity implements View.OnClickListener { static final String TAG = SetupWizardActivity.class.getSimpleName(); + // For debugging purpose. + private static final boolean FORCE_TO_SHOW_WELCOME_SCREEN = false; private static final boolean ENABLE_WELCOME_VIDEO = true; private InputMethodManager mImm; @@ -80,7 +84,7 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL private final InputMethodManager mImmInHandler; - public SettingsPoolingHandler(final SetupWizardActivity ownerInstance, + public SettingsPoolingHandler(@Nonnull final SetupWizardActivity ownerInstance, final InputMethodManager imm) { super(ownerInstance); mImmInHandler = imm; @@ -304,6 +308,9 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL private int determineSetupStepNumber() { mHandler.cancelPollingImeSettings(); + if (FORCE_TO_SHOW_WELCOME_SCREEN) { + return STEP_1; + } if (!UncachedInputMethodManagerUtils.isThisImeEnabled(this, mImm)) { return STEP_1; } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 90398deb2..2a4e14ca7 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -16,14 +16,11 @@ package com.android.inputmethod.latin.spellcheck; -import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.service.textservice.SpellCheckerService; import android.text.InputType; -import android.util.Log; -import android.util.LruCache; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.textservice.SuggestionsInfo; @@ -32,39 +29,21 @@ import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardLayoutSet; import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.ContactsBinaryDictionary; -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.DictionaryCollection; import com.android.inputmethod.latin.DictionaryFacilitator; -import com.android.inputmethod.latin.DictionaryFactory; -import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.DictionaryFacilitatorLruCache; +import com.android.inputmethod.latin.NgramContext; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; -import com.android.inputmethod.latin.UserBinaryDictionary; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; -import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.ScriptUtils; -import com.android.inputmethod.latin.utils.StringUtils; import com.android.inputmethod.latin.utils.SuggestionResults; import com.android.inputmethod.latin.WordComposer; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; import java.util.Locale; -import java.util.Map; -import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; /** * Service for spell checking, using LatinIME's dictionaries and mechanisms. @@ -80,61 +59,28 @@ public final class AndroidSpellCheckerService extends SpellCheckerService private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 368; private static final String DICTIONARY_NAME_PREFIX = "spellcheck_"; - private static final int WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS = 1000; - private static final int MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT = 5; private static final String[] EMPTY_STRING_ARRAY = new String[0]; - private final HashSet<Locale> mCachedLocales = new HashSet<>(); - private final int MAX_NUM_OF_THREADS_READ_DICTIONARY = 2; private final Semaphore mSemaphore = new Semaphore(MAX_NUM_OF_THREADS_READ_DICTIONARY, true /* fair */); // TODO: Make each spell checker session has its own session id. private final ConcurrentLinkedQueue<Integer> mSessionIdPool = new ConcurrentLinkedQueue<>(); - private static class DictionaryFacilitatorLruCache extends - LruCache<Locale, DictionaryFacilitator> { - private final HashSet<Locale> mCachedLocales; - public DictionaryFacilitatorLruCache(final HashSet<Locale> cachedLocales, int maxSize) { - super(maxSize); - mCachedLocales = cachedLocales; - } - - @Override - protected void entryRemoved(boolean evicted, Locale key, - DictionaryFacilitator oldValue, DictionaryFacilitator newValue) { - if (oldValue != null && oldValue != newValue) { - oldValue.closeDictionaries(); - } - if (key != null && newValue == null) { - // Remove locale from the cache when the dictionary facilitator for the locale is - // evicted and new facilitator is not set for the locale. - mCachedLocales.remove(key); - if (size() >= maxSize()) { - Log.w(TAG, "DictionaryFacilitator for " + key.toString() - + " has been evicted due to cache size limit." - + " size: " + size() + ", maxSize: " + maxSize()); - } - } - } - } - private static final int MAX_DICTIONARY_FACILITATOR_COUNT = 3; - private final LruCache<Locale, DictionaryFacilitator> mDictionaryFacilitatorCache = - new DictionaryFacilitatorLruCache(mCachedLocales, MAX_DICTIONARY_FACILITATOR_COUNT); + private final DictionaryFacilitatorLruCache mDictionaryFacilitatorCache = + new DictionaryFacilitatorLruCache(this /* context */, MAX_DICTIONARY_FACILITATOR_COUNT, + DICTIONARY_NAME_PREFIX); private final ConcurrentHashMap<Locale, Keyboard> mKeyboardCache = new ConcurrentHashMap<>(); // The threshold for a suggestion to be considered "recommended". private float mRecommendedThreshold; - // Whether to use the contacts dictionary - private boolean mUseContactsDictionary; // TODO: make a spell checker option to block offensive words or not private final SettingsValuesForSuggestion mSettingsValuesForSuggestion = new SettingsValuesForSuggestion(true /* blockPotentiallyOffensive */, true /* spaceAwareGestureEnabled */, null /* additionalFeaturesSettingValues */); - private final Object mDictionaryLock = new Object(); public static final String SINGLE_QUOTE = "\u0027"; public static final String APOSTROPHE = "\u2019"; @@ -176,20 +122,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { if (!PREF_USE_CONTACTS_KEY.equals(key)) return; final boolean useContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); - if (useContactsDictionary != mUseContactsDictionary) { - mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY); - try { - mUseContactsDictionary = useContactsDictionary; - for (final Locale locale : mCachedLocales) { - final DictionaryFacilitator dictionaryFacilitator = - mDictionaryFacilitatorCache.get(locale); - resetDictionariesForLocale(this /* context */, - dictionaryFacilitator, locale, mUseContactsDictionary); - } - } finally { - mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); - } - } + mDictionaryFacilitatorCache.setUseContactsDictionary(useContactsDictionary); } @Override @@ -222,7 +155,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService mSemaphore.acquireUninterruptibly(); try { DictionaryFacilitator dictionaryFacilitatorForLocale = - getDictionaryFacilitatorForLocaleLocked(locale); + mDictionaryFacilitatorCache.get(locale); return dictionaryFacilitatorForLocale.isValidWord(word, false /* igroreCase */); } finally { mSemaphore.release(); @@ -230,14 +163,14 @@ public final class AndroidSpellCheckerService extends SpellCheckerService } public SuggestionResults getSuggestionResults(final Locale locale, final WordComposer composer, - final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo) { + final NgramContext ngramContext, final ProximityInfo proximityInfo) { Integer sessionId = null; mSemaphore.acquireUninterruptibly(); try { sessionId = mSessionIdPool.poll(); DictionaryFacilitator dictionaryFacilitatorForLocale = - getDictionaryFacilitatorForLocaleLocked(locale); - return dictionaryFacilitatorForLocale.getSuggestionResults(composer, prevWordsInfo, + mDictionaryFacilitatorCache.get(locale); + return dictionaryFacilitatorForLocale.getSuggestionResults(composer, ngramContext, proximityInfo, mSettingsValuesForSuggestion, sessionId); } finally { if (sessionId != null) { @@ -251,56 +184,18 @@ public final class AndroidSpellCheckerService extends SpellCheckerService mSemaphore.acquireUninterruptibly(); try { final DictionaryFacilitator dictionaryFacilitator = - getDictionaryFacilitatorForLocaleLocked(locale); - return dictionaryFacilitator.hasInitializedMainDictionary(); + mDictionaryFacilitatorCache.get(locale); + return dictionaryFacilitator.hasAtLeastOneInitializedMainDictionary(); } finally { mSemaphore.release(); } } - private DictionaryFacilitator getDictionaryFacilitatorForLocaleLocked(final Locale locale) { - DictionaryFacilitator dictionaryFacilitatorForLocale = - mDictionaryFacilitatorCache.get(locale); - if (dictionaryFacilitatorForLocale == null) { - dictionaryFacilitatorForLocale = new DictionaryFacilitator(); - mDictionaryFacilitatorCache.put(locale, dictionaryFacilitatorForLocale); - mCachedLocales.add(locale); - resetDictionariesForLocale(this /* context */, dictionaryFacilitatorForLocale, - locale, mUseContactsDictionary); - } - return dictionaryFacilitatorForLocale; - } - - private static void resetDictionariesForLocale(final Context context, - final DictionaryFacilitator dictionaryFacilitator, final Locale locale, - final boolean useContactsDictionary) { - dictionaryFacilitator.resetDictionariesWithDictNamePrefix(context, locale, - useContactsDictionary, false /* usePersonalizedDicts */, - false /* forceReloadMainDictionary */, null /* listener */, - DICTIONARY_NAME_PREFIX); - for (int i = 0; i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT; i++) { - try { - dictionaryFacilitator.waitForLoadingMainDictionary( - WAIT_FOR_LOADING_MAIN_DICT_IN_MILLISECONDS, TimeUnit.MILLISECONDS); - return; - } catch (final InterruptedException e) { - Log.i(TAG, "Interrupted during waiting for loading main dictionary.", e); - if (i < MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT - 1) { - Log.i(TAG, "Retry", e); - } else { - Log.w(TAG, "Give up retrying. Retried " - + MAX_RETRY_COUNT_FOR_WAITING_FOR_LOADING_DICT + " times.", e); - } - } - } - } - @Override public boolean onUnbind(final Intent intent) { mSemaphore.acquireUninterruptibly(MAX_NUM_OF_THREADS_READ_DICTIONARY); try { mDictionaryFacilitatorCache.evictAll(); - mCachedLocales.clear(); } finally { mSemaphore.release(MAX_NUM_OF_THREADS_READ_DICTIONARY); } @@ -334,7 +229,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(this, editorInfo); builder.setKeyboardGeometry( SPELLCHECKER_DUMMY_KEYBOARD_WIDTH, SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT); - builder.setSubtype(subtype); + builder.setSubtype(new RichInputMethodSubtype(subtype)); builder.setIsSpellChecker(true /* isSpellChecker */); builder.disableTouchPositionCorrectionData(); return builder.build(); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java index 34e01197a..8393b306c 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java @@ -25,7 +25,7 @@ import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; import com.android.inputmethod.compat.TextInfoCompatUtils; -import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.NgramContext; import com.android.inputmethod.latin.utils.StringUtils; import java.util.ArrayList; @@ -62,8 +62,8 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck final int offset = ssi.getOffsetAt(i); final int length = ssi.getLengthAt(i); final CharSequence subText = typedText.subSequence(offset, offset + length); - final PrevWordsInfo prevWordsInfo = - new PrevWordsInfo(new PrevWordsInfo.WordInfo(currentWord)); + final NgramContext ngramContext = + new NgramContext(new NgramContext.WordInfo(currentWord)); currentWord = subText; if (!subText.toString().contains(AndroidSpellCheckerService.SINGLE_QUOTE)) { continue; @@ -80,7 +80,7 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck if (TextUtils.isEmpty(splitText)) { continue; } - if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString(), prevWordsInfo) + if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString(), ngramContext) == null) { continue; } @@ -208,10 +208,10 @@ public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheck } else { prevWord = null; } - final PrevWordsInfo prevWordsInfo = - new PrevWordsInfo(new PrevWordsInfo.WordInfo(prevWord)); + final NgramContext ngramContext = + new NgramContext(new NgramContext.WordInfo(prevWord)); final TextInfo textInfo = textInfos[i]; - retval[i] = onGetSuggestionsInternal(textInfo, prevWordsInfo, suggestionsLimit); + retval[i] = onGetSuggestionsInternal(textInfo, ngramContext, suggestionsLimit); retval[i].setCookieAndSequence(textInfo.getCookie(), textInfo.getSequence()); } return retval; diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index d668672aa..7b6aacd15 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -31,7 +31,7 @@ import com.android.inputmethod.compat.SuggestionsInfoCompatUtils; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.NgramContext; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; @@ -73,27 +73,25 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache = new LruCache<>(MAX_CACHE_SIZE); - // TODO: Support n-gram input - private static String generateKey(final String query, final PrevWordsInfo prevWordsInfo) { - if (TextUtils.isEmpty(query) || !prevWordsInfo.isValid()) { + private static String generateKey(final String query, final NgramContext ngramContext) { + if (TextUtils.isEmpty(query) || !ngramContext.isValid()) { return query; } - return query + CHAR_DELIMITER + prevWordsInfo; + return query + CHAR_DELIMITER + ngramContext; } public SuggestionsParams getSuggestionsFromCache(String query, - final PrevWordsInfo prevWordsInfo) { - return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWordsInfo)); + final NgramContext ngramContext) { + return mUnigramSuggestionsInfoCache.get(generateKey(query, ngramContext)); } - public void putSuggestionsToCache( - final String query, final PrevWordsInfo prevWordsInfo, + public void putSuggestionsToCache(final String query, final NgramContext ngramContext, final String[] suggestions, final int flags) { if (suggestions == null || TextUtils.isEmpty(query)) { return; } mUnigramSuggestionsInfoCache.put( - generateKey(query, prevWordsInfo), new SuggestionsParams(suggestions, flags)); + generateKey(query, ngramContext), new SuggestionsParams(suggestions, flags)); } public void clearCache() { @@ -223,12 +221,11 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { } protected SuggestionsInfo onGetSuggestionsInternal( - final TextInfo textInfo, final PrevWordsInfo prevWordsInfo, - final int suggestionsLimit) { + final TextInfo textInfo, final NgramContext ngramContext, final int suggestionsLimit) { try { final String inText = textInfo.getText(); final SuggestionsParams cachedSuggestionsParams = - mSuggestionsCache.getSuggestionsFromCache(inText, prevWordsInfo); + mSuggestionsCache.getSuggestionsFromCache(inText, ngramContext); if (cachedSuggestionsParams != null) { if (DBG) { Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags); @@ -283,7 +280,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { composer.setComposingWord(codePoints, coordinates); // TODO: Don't gather suggestions if the limit is <= 0 unless necessary final SuggestionResults suggestionResults = mService.getSuggestionResults( - mLocale, composer, prevWordsInfo, proximityInfo); + mLocale, composer, ngramContext, proximityInfo); final Result result = getResult(capitalizeType, mLocale, suggestionsLimit, mService.getRecommendedThreshold(), text, suggestionResults); isInDict = isInDictForAnyCapitalization(text, capitalizeType); @@ -308,7 +305,7 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() : 0); final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions); - mSuggestionsCache.putSuggestionsToCache(text, prevWordsInfo, result.mSuggestions, + mSuggestionsCache.putSuggestionsToCache(text, ngramContext, result.mSuggestions, flags); return retval; } catch (RuntimeException e) { diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java b/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java index 51c4b1ee8..9ddee8629 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SentenceLevelAdapter.java @@ -145,9 +145,8 @@ public class SentenceLevelAdapter { int wordEnd = wordIterator.getEndOfWord(originalText, wordStart); while (wordStart <= end && wordEnd != -1 && wordStart != -1) { if (wordEnd >= start && wordEnd > wordStart) { - CharSequence subSequence = originalText.subSequence(wordStart, wordEnd).toString(); - final TextInfo ti = TextInfoCompatUtils.newInstance(subSequence, 0, - subSequence.length(), cookie, subSequence.hashCode()); + final TextInfo ti = TextInfoCompatUtils.newInstance(originalText, wordStart, + wordEnd, cookie, originalText.subSequence(wordStart, wordEnd).hashCode()); wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd)); } wordStart = wordIterator.getBeginningOfNextWord(originalText, wordEnd); diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java index f7b6f919d..907e3fa42 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java @@ -40,6 +40,8 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView { public abstract void onSuggestionSelected(final SuggestedWordInfo info); } + private boolean mIsInModalMode; + public MoreSuggestionsView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.moreKeysKeyboardViewStyle); } @@ -53,6 +55,7 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView { @Override public void setKeyboard(final Keyboard keyboard) { super.setKeyboard(keyboard); + mIsInModalMode = false; // With accessibility mode off, {@link #mAccessibilityDelegate} is set to null at the // above {@link MoreKeysKeyboardView#setKeyboard(Keyboard)} call. // With accessibility mode on, {@link #mAccessibilityDelegate} is set to a @@ -74,12 +77,17 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView { updateKeyDrawParams(keyHeight); } - public void adjustVerticalCorrectionForModalMode() { + public void setModalMode() { + mIsInModalMode = true; // Set vertical correction to zero (Reset more keys keyboard sliding allowance // {@link R#dimen.config_more_keys_keyboard_slide_allowance}). mKeyDetector.setKeyboard(getKeyboard(), -getPaddingLeft(), -getPaddingTop()); } + public boolean isInModalMode() { + return mIsInModalMode; + } + @Override protected void onKeyInput(final Key key, final int x, final int y) { if (!(key instanceof MoreSuggestionKey)) { diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java index 1e8df8986..7b66bbb75 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java @@ -60,6 +60,9 @@ import com.android.inputmethod.latin.utils.ViewLayoutUtils; import java.util.ArrayList; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + final class SuggestionStripLayoutHelper { private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3; private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f; @@ -213,15 +216,14 @@ final class SuggestionStripLayoutHelper { return word; } - final int len = word.length(); final Spannable spannedWord = new SpannableString(word); final int options = mSuggestionStripOptions; if ((isAutoCorrection && (options & AUTO_CORRECT_BOLD) != 0) || (isTypedWordValid && (options & VALID_TYPED_WORD_BOLD) != 0)) { - spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + addStyleSpan(spannedWord, BOLD_SPAN); } if (isAutoCorrection && (options & AUTO_CORRECT_UNDERLINE) != 0) { - spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + addStyleSpan(spannedWord, UNDERLINE_SPAN); } return spannedWord; } @@ -238,9 +240,9 @@ final class SuggestionStripLayoutHelper { final SettingsValues settingsValues = Settings.getInstance().getCurrent(); final boolean shouldOmitTypedWord = shouldOmitTypedWord(suggestedWords.mInputStyle, settingsValues.mGestureFloatingPreviewTextEnabled, - settingsValues.mShouldShowUiToAcceptTypedWord); + settingsValues.mShouldShowLxxSuggestionUi); return getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords.mWillAutoCorrect, - settingsValues.mShouldShowUiToAcceptTypedWord && shouldOmitTypedWord, + settingsValues.mShouldShowLxxSuggestionUi && shouldOmitTypedWord, mCenterPositionInStrip, mTypedWordPositionWhenAutocorrect); } @@ -319,18 +321,6 @@ final class SuggestionStripLayoutHelper { } else { color = mColorSuggested; } - if (DebugFlags.DEBUG_ENABLED && suggestedWords.size() > 1) { - // If we auto-correct, then the autocorrection is in slot 0 and the typed word - // is in slot 1. - if (indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION - && suggestedWords.mWillAutoCorrect - && AutoCorrectionUtils.shouldBlockAutoCorrectionBySafetyNet( - suggestedWords.getLabel(SuggestedWords.INDEX_OF_AUTO_CORRECTION), - suggestedWords.getLabel(SuggestedWords.INDEX_OF_TYPED_WORD))) { - return 0xFFFF0000; - } - } - if (suggestedWords.mIsObsoleteSuggestions && !isTypedWord) { return applyAlpha(color, mAlphaObsoleted); } @@ -365,17 +355,19 @@ final class SuggestionStripLayoutHelper { (PunctuationSuggestions)suggestedWords, stripView); } + final int wordCountToShow = suggestedWords.getWordCountToShow( + Settings.getInstance().getCurrent().mShouldShowLxxSuggestionUi); final int startIndexOfMoreSuggestions = setupWordViewsAndReturnStartIndexOfMoreSuggestions( suggestedWords, mSuggestionsCountInStrip); final TextView centerWordView = mWordViews.get(mCenterPositionInStrip); final int stripWidth = stripView.getWidth(); final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, stripWidth); - if (suggestedWords.size() == 1 || getTextScaleX(centerWordView.getText(), centerWidth, + if (wordCountToShow == 1 || getTextScaleX(centerWordView.getText(), centerWidth, centerWordView.getPaint()) < MIN_TEXT_XSCALE) { // Layout only the most relevant suggested word at the center of the suggestion strip // by consolidating all slots in the strip. final int countInStrip = 1; - mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); + mMoreSuggestionsAvailable = (wordCountToShow > countInStrip); layoutWord(mCenterPositionInStrip, stripWidth - mPadding); stripView.addView(centerWordView); setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT); @@ -387,7 +379,7 @@ final class SuggestionStripLayoutHelper { } final int countInStrip = mSuggestionsCountInStrip; - mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); + mMoreSuggestionsAvailable = (wordCountToShow > countInStrip); int x = 0; for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { if (positionInStrip != 0) { @@ -442,10 +434,11 @@ final class SuggestionStripLayoutHelper { // {@link StyleSpan} in a content description may cause an issue of TTS/TalkBack. // Use a simple {@link String} to avoid the issue. wordView.setContentDescription(TextUtils.isEmpty(word) ? null : word.toString()); - final CharSequence text = getEllipsizedText(word, width, wordView.getPaint()); - final float scaleX = getTextScaleX(word, width, wordView.getPaint()); + final CharSequence text = getEllipsizedTextWithSettingScaleX( + word, width, wordView.getPaint()); + final float scaleX = wordView.getTextScaleX(); wordView.setText(text); // TextView.setText() resets text scale x to 1.0. - wordView.setTextScaleX(Math.max(scaleX, MIN_TEXT_XSCALE)); + wordView.setTextScaleX(scaleX); // A <code>wordView</code> should be disabled when <code>word</code> is empty in order to // make it unclickable. // With accessibility touch exploration on, <code>wordView</code> should be enabled even @@ -548,22 +541,23 @@ final class SuggestionStripLayoutHelper { return countInStrip; } - public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip) { - final boolean shouldShowUiToAcceptTypedWord = Settings.getInstance().getCurrent() - .mShouldShowUiToAcceptTypedWord; + public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip, + final boolean shouldShowWordToSave) { + final boolean showsHintWithWord = shouldShowWordToSave + || !Settings.getInstance().getCurrent().mShouldShowLxxSuggestionUi; final int stripWidth = addToDictionaryStrip.getWidth(); - final int width = shouldShowUiToAcceptTypedWord ? stripWidth - : stripWidth - mDividerWidth - mPadding * 2; + final int width = stripWidth - (showsHintWithWord ? mDividerWidth + mPadding * 2 : 0); final TextView wordView = (TextView)addToDictionaryStrip.findViewById(R.id.word_to_save); wordView.setTextColor(mColorTypedWord); final int wordWidth = (int)(width * mCenterSuggestionWeight); - final CharSequence wordToSave = getEllipsizedText(word, wordWidth, wordView.getPaint()); + final CharSequence wordToSave = getEllipsizedTextWithSettingScaleX( + word, wordWidth, wordView.getPaint()); final float wordScaleX = wordView.getTextScaleX(); wordView.setText(wordToSave); wordView.setTextScaleX(wordScaleX); setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); - final int wordVisibility = shouldShowUiToAcceptTypedWord ? View.GONE : View.VISIBLE; + final int wordVisibility = showsHintWithWord ? View.VISIBLE : View.GONE; wordView.setVisibility(wordVisibility); addToDictionaryStrip.findViewById(R.id.word_to_save_divider).setVisibility(wordVisibility); @@ -573,12 +567,7 @@ final class SuggestionStripLayoutHelper { final float hintWeight; final TextView hintView = (TextView)addToDictionaryStrip.findViewById( R.id.hint_add_to_dictionary); - if (shouldShowUiToAcceptTypedWord) { - hintText = res.getText(R.string.hint_add_to_dictionary_without_word); - hintWidth = width; - hintWeight = 1.0f; - hintView.setGravity(Gravity.CENTER); - } else { + if (showsHintWithWord) { final boolean isRtlLanguage = (ViewCompat.getLayoutDirection(addToDictionaryStrip) == ViewCompat.LAYOUT_DIRECTION_RTL); final String arrow = isRtlLanguage ? RIGHTWARDS_ARROW : LEFTWARDS_ARROW; @@ -589,10 +578,15 @@ final class SuggestionStripLayoutHelper { hintWidth = width - wordWidth; hintWeight = 1.0f - mCenterSuggestionWeight; hintView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); + } else { + hintText = res.getText(R.string.hint_add_to_dictionary_without_word); + hintWidth = width; + hintWeight = 1.0f; + hintView.setGravity(Gravity.CENTER); } hintView.setTextColor(mColorAutoCorrect); final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint()); - hintView.setText(hintText); + hintView.setText(hintText); // TextView.setText() resets text scale x to 1.0. hintView.setTextScaleX(hintScaleX); setLayoutWeight(hintView, hintWeight, ViewGroup.LayoutParams.MATCH_PARENT); } @@ -604,8 +598,7 @@ final class SuggestionStripLayoutHelper { final int width = titleView.getWidth() - titleView.getPaddingLeft() - titleView.getPaddingRight(); titleView.setTextColor(mColorAutoCorrect); - titleView.setText(importantNoticeTitle); - titleView.setTextScaleX(1.0f); // Reset textScaleX. + titleView.setText(importantNoticeTitle); // TextView.setText() resets text scale x to 1.0. final float titleScaleX = getTextScaleX(importantNoticeTitle, width, titleView.getPaint()); titleView.setTextScaleX(titleScaleX); } @@ -620,18 +613,19 @@ final class SuggestionStripLayoutHelper { } } - private static float getTextScaleX(final CharSequence text, final int maxWidth, + private static float getTextScaleX(@Nullable final CharSequence text, final int maxWidth, final TextPaint paint) { paint.setTextScaleX(1.0f); final int width = getTextWidth(text, paint); if (width <= maxWidth || maxWidth <= 0) { return 1.0f; } - return maxWidth / (float)width; + return maxWidth / (float) width; } - private static CharSequence getEllipsizedText(final CharSequence text, final int maxWidth, - final TextPaint paint) { + @Nullable + private static CharSequence getEllipsizedTextWithSettingScaleX( + @Nullable final CharSequence text, final int maxWidth, @Nonnull final TextPaint paint) { if (text == null) { return null; } @@ -641,62 +635,63 @@ final class SuggestionStripLayoutHelper { return text; } - // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To - // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE). - final float upscaledWidth = maxWidth / MIN_TEXT_XSCALE; - CharSequence ellipsized = TextUtils.ellipsize( - text, paint, upscaledWidth, TextUtils.TruncateAt.MIDDLE); - // For an unknown reason, ellipsized seems to return a text that does indeed fit inside the - // passed width according to paint.measureText, but not according to paint.getTextWidths. - // But when rendered, the text seems to actually take up as many pixels as returned by - // paint.getTextWidths, hence problem. - // To save this case, we compare the measured size of the new text, and if it's too much, - // try it again removing the difference. This may still give a text too long by one or - // two pixels so we take an additional 2 pixels cushion and call it a day. - // TODO: figure out why getTextWidths and measureText don't agree with each other, and - // remove the following code. - final float ellipsizedTextWidth = getTextWidth(ellipsized, paint); - if (upscaledWidth <= ellipsizedTextWidth) { - ellipsized = TextUtils.ellipsize( - text, paint, upscaledWidth - (ellipsizedTextWidth - upscaledWidth) - 2, - TextUtils.TruncateAt.MIDDLE); - } + // <code>text</code> must be ellipsized with minimum text scale x. paint.setTextScaleX(MIN_TEXT_XSCALE); - return ellipsized; + final boolean hasBoldStyle = hasStyleSpan(text, BOLD_SPAN); + final boolean hasUnderlineStyle = hasStyleSpan(text, UNDERLINE_SPAN); + // TextUtils.ellipsize erases any span object existed after ellipsized point. + // We have to restore these spans afterward. + final CharSequence ellipsizedText = TextUtils.ellipsize( + text, paint, maxWidth, TextUtils.TruncateAt.MIDDLE); + if (!hasBoldStyle && !hasUnderlineStyle) { + return ellipsizedText; + } + final Spannable spannableText = (ellipsizedText instanceof Spannable) + ? (Spannable)ellipsizedText : new SpannableString(ellipsizedText); + if (hasBoldStyle) { + addStyleSpan(spannableText, BOLD_SPAN); + } + if (hasUnderlineStyle) { + addStyleSpan(spannableText, UNDERLINE_SPAN); + } + return spannableText; + } + + private static boolean hasStyleSpan(@Nullable final CharSequence text, + final CharacterStyle style) { + if (text instanceof Spanned) { + return ((Spanned)text).getSpanStart(style) >= 0; + } + return false; + } + + private static void addStyleSpan(@Nonnull final Spannable text, final CharacterStyle style) { + text.removeSpan(style); + text.setSpan(style, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } - private static int getTextWidth(final CharSequence text, final TextPaint paint) { + private static int getTextWidth(@Nullable final CharSequence text, final TextPaint paint) { if (TextUtils.isEmpty(text)) { return 0; } + final int length = text.length(); + final float[] widths = new float[length]; + final int count; final Typeface savedTypeface = paint.getTypeface(); - paint.setTypeface(getTextTypeface(text)); - final int len = text.length(); - final float[] widths = new float[len]; - final int count = paint.getTextWidths(text, 0, len, widths); + try { + paint.setTypeface(getTextTypeface(text)); + count = paint.getTextWidths(text, 0, length, widths); + } finally { + paint.setTypeface(savedTypeface); + } int width = 0; for (int i = 0; i < count; i++) { width += Math.round(widths[i] + 0.5f); } - paint.setTypeface(savedTypeface); return width; } - private static Typeface getTextTypeface(final CharSequence text) { - if (!(text instanceof SpannableString)) { - return Typeface.DEFAULT; - } - - final SpannableString ss = (SpannableString)text; - final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class); - if (styles.length == 0) { - return Typeface.DEFAULT; - } - - if (styles[0].getStyle() == Typeface.BOLD) { - return Typeface.DEFAULT_BOLD; - } - // TODO: BOLD_ITALIC, ITALIC case? - return Typeface.DEFAULT; + private static Typeface getTextTypeface(@Nullable final CharSequence text) { + return hasStyleSpan(text, BOLD_SPAN) ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT; } } diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index 0fd5e139e..789d549d7 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -82,7 +82,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick private final ArrayList<View> mDividerViews = new ArrayList<>(); Listener mListener; - private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; + private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance(); private int mStartIndexOfMoreSuggestions; private final SuggestionStripLayoutHelper mLayoutHelper; @@ -131,6 +131,10 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mImportantNoticeStrip.setVisibility(VISIBLE); } + public boolean isShowingImportantNoticeStrip() { + return mImportantNoticeStrip.getVisibility() == VISIBLE; + } + public boolean isShowingAddToDictionaryStrip() { return mAddToDictionaryStrip.getVisibility() == VISIBLE; } @@ -227,8 +231,8 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick return mStripVisibilityGroup.isShowingAddToDictionaryStrip(); } - public void showAddToDictionaryHint(final String word) { - mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip); + public void showAddToDictionaryHint(final String word, final boolean shouldShowWordToSave) { + mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip, shouldShowWordToSave); // {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word // will be extracted at {@link #onClick(View)}. mAddToDictionaryStrip.setTag(word); @@ -393,11 +397,18 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick @Override public boolean onInterceptTouchEvent(final MotionEvent me) { + if (mStripVisibilityGroup.isShowingImportantNoticeStrip()) { + return false; + } + // Detecting sliding up finger to show {@link MoreSuggestionsView}. if (!mMoreSuggestionsView.isShowingInParent()) { mLastX = (int)me.getX(); mLastY = (int)me.getY(); return mMoreSuggestionsSlidingDetector.onTouchEvent(me); } + if (mMoreSuggestionsView.isInModalMode()) { + return false; + } final int action = me.getAction(); final int index = me.getActionIndex(); @@ -416,7 +427,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { // Decided to be in the modal input mode. - mMoreSuggestionsView.adjustVerticalCorrectionForModalMode(); + mMoreSuggestionsView.setModalMode(); } return false; } @@ -429,6 +440,11 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick @Override public boolean onTouchEvent(final MotionEvent me) { + if (!mMoreSuggestionsView.isShowingInParent()) { + // Ignore any touch event while more suggestions panel hasn't been shown. + // Detecting sliding up is done at {@link #onInterceptTouchEvent}. + return true; + } // In the sliding input mode. {@link MotionEvent} should be forwarded to // {@link MoreSuggestionsView}. final int index = me.getActionIndex(); @@ -485,7 +501,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick return; } final Object tag = view.getTag(); - // {@link String} tag is set at {@link #showAddToDictionaryHint(String,CharSequence)}. + // {@link String} tag is set at {@link #suggestAddingToDictionary(String,CharSequence)}. if (tag instanceof String) { final String wordToSave = (String)tag; mListener.addWordToUserDictionary(wordToSave); diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java index 52708455e..5c86a02af 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java @@ -22,7 +22,7 @@ import com.android.inputmethod.latin.SuggestedWords; * An object that gives basic control of a suggestion strip and some info on it. */ public interface SuggestionStripViewAccessor { - public void showAddToDictionaryHint(final String word); + public void suggestAddingToDictionary(final String word, final boolean isFromSuggestionStrip); public boolean isShowingAddToDictionaryHint(); public void dismissAddToDictionaryHint(); public void setNeutralSuggestionStrip(); diff --git a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java index 156fcf57c..cba769521 100644 --- a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java @@ -52,41 +52,9 @@ public final class AutoCorrectionUtils { if (DBG) { Log.d(TAG, "Auto corrected by S-threshold."); } - return !shouldBlockAutoCorrectionBySafetyNet(consideredWord, suggestion.mWord); + return true; } } return false; } - - // TODO: Resolve the inconsistencies between the native auto correction algorithms and - // this safety net - public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord, - final String suggestion) { - // Safety net for auto correction. - // Actually if we hit this safety net, it's a bug. - // If user selected aggressive auto correction mode, there is no need to use the safety - // net. - // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH, - // we should not use net because relatively edit distance can be big. - final int typedWordLength = typedWord.length(); - if (typedWordLength < MINIMUM_SAFETY_NET_CHAR_LENGTH) { - return false; - } - final int maxEditDistanceOfNativeDictionary = (typedWordLength / 2) + 1; - final int distance = BinaryDictionaryUtils.editDistance(typedWord, suggestion); - if (DBG) { - Log.d(TAG, "Autocorrected edit distance = " + distance - + ", " + maxEditDistanceOfNativeDictionary); - } - if (distance > maxEditDistanceOfNativeDictionary) { - if (DBG) { - Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestion); - Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. " - + "Turning off auto-correction."); - } - return true; - } else { - return false; - } - } } diff --git a/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java index 5d7deba15..ce25fe6a4 100644 --- a/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java @@ -43,7 +43,6 @@ public final class BinaryDictionaryUtils { private static native boolean createEmptyDictFileNative(String filePath, long dictVersion, String locale, String[] attributeKeyStringArray, String[] attributeValueStringArray); private static native float calcNormalizedScoreNative(int[] before, int[] after, int score); - private static native int editDistanceNative(int[] before, int[] after); private static native int setCurrentTimeForTestNative(int currentTime); public static DictionaryHeader getHeader(final File dictFile) @@ -112,14 +111,6 @@ public final class BinaryDictionaryUtils { StringUtils.toCodePointArray(after), score); } - public static int editDistance(final String before, final String after) { - if (before == null || after == null) { - throw new IllegalArgumentException(); - } - return editDistanceNative(StringUtils.toCodePointArray(before), - StringUtils.toCodePointArray(after)); - } - /** * Control the current time to be used in the native code. If currentTime >= 0, this method sets * the current time and gets into test mode. 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; } diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java index 61292fc36..fb36b7c50 100644 --- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin.utils; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.TreeMap; @@ -40,4 +41,13 @@ public final class CollectionUtils { } return list; } + + /** + * Tests whether c contains no elements, true if c is null or c is empty. + * @param c Collection to test. + * @return Whether c contains no elements. + */ + public static boolean isNullOrEmpty(final Collection c) { + return c == null || c.isEmpty(); + } } diff --git a/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java index 34f59e8bc..7e8e55990 100644 --- a/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java @@ -67,7 +67,7 @@ public class CombinedFormatUtils { builder.append("," + BLACKLISTED_TAG + "=true"); } builder.append("\n"); - if (wordProperty.mShortcutTargets != null) { + if (wordProperty.mHasShortcuts) { for (final WeightedString shortcutTarget : wordProperty.mShortcutTargets) { builder.append(" " + SHORTCUT_TAG + "=" + shortcutTarget.mWord); builder.append(","); @@ -75,8 +75,9 @@ public class CombinedFormatUtils { builder.append("\n"); } } - if (wordProperty.mBigrams != null) { - for (final WeightedString bigram : wordProperty.mBigrams) { + if (wordProperty.mHasNgrams) { + // TODO: Support ngram. + for (final WeightedString bigram : wordProperty.getBigrams()) { builder.append(" " + BIGRAM_TAG + "=" + bigram.mWord); builder.append(","); builder.append(formatProbabilityInfo(bigram.mProbabilityInfo)); diff --git a/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java index 9dc0524a2..e05618901 100644 --- a/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CursorAnchorInfoUtils.java @@ -16,10 +16,12 @@ package com.android.inputmethod.latin.utils; +import android.annotation.TargetApi; import android.graphics.Matrix; import android.graphics.Rect; import android.inputmethodservice.ExtractEditText; import android.inputmethodservice.InputMethodService; +import android.os.Build; import android.text.Layout; import android.text.Spannable; import android.view.View; @@ -27,6 +29,12 @@ import android.view.ViewParent; import android.view.inputmethod.CursorAnchorInfo; import android.widget.TextView; +import com.android.inputmethod.compat.BuildCompatUtils; +import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * This class allows input methods to extract {@link CursorAnchorInfo} directly from the given * {@link TextView}. This is useful and even necessary to support full-screen mode where the default @@ -77,13 +85,32 @@ public final class CursorAnchorInfoUtils { } /** + * Extracts {@link CursorAnchorInfoCompatWrapper} from the given {@link TextView}. + * @param textView the target text view from which {@link CursorAnchorInfoCompatWrapper} is to + * be extracted. + * @return the {@link CursorAnchorInfoCompatWrapper} object based on the current layout. + * {@code null} if {@code Build.VERSION.SDK_INT} is 20 or prior or {@link TextView} is not + * ready to provide layout information. + */ + @Nullable + public static CursorAnchorInfoCompatWrapper extractFromTextView( + @Nonnull final TextView textView) { + if (Build.VERSION.SDK_INT < BuildCompatUtils.VERSION_CODES_LXX) { + return null; + } + return CursorAnchorInfoCompatWrapper.wrap(extractFromTextViewInternal(textView)); + } + + /** * Returns {@link CursorAnchorInfo} from the given {@link TextView}. * @param textView the target text view from which {@link CursorAnchorInfo} is to be extracted. * @return the {@link CursorAnchorInfo} object based on the current layout. {@code null} if it * is not feasible. */ - public static CursorAnchorInfo getCursorAnchorInfo(final TextView textView) { - Layout layout = textView.getLayout(); + @TargetApi(BuildCompatUtils.VERSION_CODES_LXX) + @Nullable + private static CursorAnchorInfo extractFromTextViewInternal(@Nonnull final TextView textView) { + final Layout layout = textView.getLayout(); if (layout == null) { return null; } diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java index 197908032..249478785 100644 --- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -23,6 +23,7 @@ import android.content.res.Resources; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.AssetFileAddress; import com.android.inputmethod.latin.BinaryDictionaryGetter; import com.android.inputmethod.latin.Constants; @@ -382,6 +383,7 @@ public class DictionaryInfoUtils { return dictList; } + @UsedForTesting public static boolean looksValidForDictionaryInsertion(final CharSequence text, final SpacingAndPunctuations spacingAndPunctuations) { if (TextUtils.isEmpty(text)) return false; diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java index 787e4a59d..355d00dac 100644 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java @@ -21,33 +21,69 @@ import java.util.Locale; import android.view.inputmethod.InputMethodSubtype; -import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.NgramContext; public interface DistracterFilter { /** * Determine whether a word is a distracter to words in dictionaries. * - * @param prevWordsInfo the information of previous words. + * @param ngramContext the n-gram context * @param testedWord the word that will be tested to see whether it is a distracter to words * in dictionaries. * @param locale the locale of word. * @return true if testedWord is a distracter, otherwise false. */ - public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo, + public boolean isDistracterToWordsInDictionaries(final NgramContext ngramContext, final String testedWord, final Locale locale); + @UsedForTesting + public int getWordHandlingType(final NgramContext ngramContext, final String testedWord, + final Locale locale); + public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes); public void close(); + public static final class HandlingType { + private final static int REQUIRE_NO_SPECIAL_HANDLINGS = 0x0; + private final static int SHOULD_BE_LOWER_CASED = 0x1; + private final static int SHOULD_BE_HANDLED_AS_OOV = 0x2; + + public static int getHandlingType(final boolean shouldBeLowerCased, final boolean isOov) { + int wordHandlingType = HandlingType.REQUIRE_NO_SPECIAL_HANDLINGS; + if (shouldBeLowerCased) { + wordHandlingType |= HandlingType.SHOULD_BE_LOWER_CASED; + } + if (isOov) { + wordHandlingType |= HandlingType.SHOULD_BE_HANDLED_AS_OOV; + } + return wordHandlingType; + } + + public static boolean shouldBeLowerCased(final int handlingType) { + return (handlingType & SHOULD_BE_LOWER_CASED) != 0; + } + + public static boolean shouldBeHandledAsOov(final int handlingType) { + return (handlingType & SHOULD_BE_HANDLED_AS_OOV) != 0; + } + }; + public static final DistracterFilter EMPTY_DISTRACTER_FILTER = new DistracterFilter() { @Override - public boolean isDistracterToWordsInDictionaries(PrevWordsInfo prevWordsInfo, + public boolean isDistracterToWordsInDictionaries(NgramContext ngramContext, String testedWord, Locale locale) { return false; } @Override + public int getWordHandlingType(final NgramContext ngramContext, + final String testedWord, final Locale locale) { + return HandlingType.REQUIRE_NO_SPECIAL_HANDLINGS; + } + + @Override public void close() { } diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java index 27973287d..8f0f9bb44 100644 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java @@ -20,13 +20,14 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.ConcurrentHashMap; import android.content.Context; import android.content.res.Resources; import android.text.InputType; import android.util.Log; import android.util.LruCache; +import android.util.Pair; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; @@ -34,7 +35,9 @@ import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.KeyboardLayoutSet; import com.android.inputmethod.latin.DictionaryFacilitator; -import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.DictionaryFacilitatorLruCache; +import com.android.inputmethod.latin.NgramContext; +import com.android.inputmethod.latin.RichInputMethodSubtype; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; @@ -48,21 +51,22 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr DistracterFilterCheckingExactMatchesAndSuggestions.class.getSimpleName(); private static final boolean DEBUG = false; - private static final long TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS = 120; - private static final int MAX_DISTRACTERS_CACHE_SIZE = 512; + private static final int MAX_DICTIONARY_FACILITATOR_CACHE_SIZE = 3; + private static final int MAX_DISTRACTERS_CACHE_SIZE = 1024; private final Context mContext; - private final Map<Locale, InputMethodSubtype> mLocaleToSubtypeMap; - private final Map<Locale, Keyboard> mLocaleToKeyboardMap; - private final DictionaryFacilitator mDictionaryFacilitator; - private final LruCache<String, Boolean> mDistractersCache; - private Keyboard mKeyboard; + private final ConcurrentHashMap<Locale, InputMethodSubtype> mLocaleToSubtypeCache; + private final ConcurrentHashMap<Locale, Keyboard> mLocaleToKeyboardCache; + private final DictionaryFacilitatorLruCache mDictionaryFacilitatorLruCache; + // The key is a pair of a locale and a word. The value indicates the word is a distracter to + // words of the locale. + private final LruCache<Pair<Locale, String>, Boolean> mDistractersCache; private final Object mLock = new Object(); // If the score of the top suggestion exceeds this value, the tested word (e.g., - // an OOV, a misspelling, or an in-vocabulary word) would be considered as a distractor to + // an OOV, a misspelling, or an in-vocabulary word) would be considered as a distracter to // words in dictionary. The greater the threshold is, the less likely the tested word would - // become a distractor, which means the tested word will be more likely to be added to + // become a distracter, which means the tested word will be more likely to be added to // the dictionary. private static final float DISTRACTER_WORD_SCORE_THRESHOLD = 0.4f; @@ -73,16 +77,19 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr */ public DistracterFilterCheckingExactMatchesAndSuggestions(final Context context) { mContext = context; - mLocaleToSubtypeMap = new HashMap<>(); - mLocaleToKeyboardMap = new HashMap<>(); - mDictionaryFacilitator = new DictionaryFacilitator(); + mLocaleToSubtypeCache = new ConcurrentHashMap<>(); + mLocaleToKeyboardCache = new ConcurrentHashMap<>(); + mDictionaryFacilitatorLruCache = new DictionaryFacilitatorLruCache(context, + MAX_DICTIONARY_FACILITATOR_CACHE_SIZE, "" /* dictionaryNamePrefix */); mDistractersCache = new LruCache<>(MAX_DISTRACTERS_CACHE_SIZE); - mKeyboard = null; } @Override public void close() { - mDictionaryFacilitator.closeDictionaries(); + mLocaleToSubtypeCache.clear(); + mLocaleToKeyboardCache.clear(); + mDictionaryFacilitatorLruCache.evictAll(); + // Don't clear mDistractersCache. } @Override @@ -99,29 +106,36 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr newLocaleToSubtypeMap.put(locale, subtype); } } - if (mLocaleToSubtypeMap.equals(newLocaleToSubtypeMap)) { + if (mLocaleToSubtypeCache.equals(newLocaleToSubtypeMap)) { // Enabled subtypes have not been changed. return; } - synchronized (mLock) { - mLocaleToSubtypeMap.clear(); - mLocaleToSubtypeMap.putAll(newLocaleToSubtypeMap); - mLocaleToKeyboardMap.clear(); + // Update subtype and keyboard map for locales that are in the current mapping. + for (final Locale locale: mLocaleToSubtypeCache.keySet()) { + if (newLocaleToSubtypeMap.containsKey(locale)) { + final InputMethodSubtype newSubtype = newLocaleToSubtypeMap.remove(locale); + if (newSubtype.equals(newLocaleToSubtypeMap.get(locale))) { + // Mapping has not been changed. + continue; + } + mLocaleToSubtypeCache.replace(locale, newSubtype); + } else { + mLocaleToSubtypeCache.remove(locale); + } + mLocaleToKeyboardCache.remove(locale); } + // Add locales that are not in the current mapping. + mLocaleToSubtypeCache.putAll(newLocaleToSubtypeMap); } - private void loadKeyboardForLocale(final Locale newLocale) { - final Keyboard cachedKeyboard = mLocaleToKeyboardMap.get(newLocale); + private Keyboard getKeyboardForLocale(final Locale locale) { + final Keyboard cachedKeyboard = mLocaleToKeyboardCache.get(locale); if (cachedKeyboard != null) { - mKeyboard = cachedKeyboard; - return; - } - final InputMethodSubtype subtype; - synchronized (mLock) { - subtype = mLocaleToSubtypeMap.get(newLocale); + return cachedKeyboard; } + final InputMethodSubtype subtype = mLocaleToSubtypeCache.get(locale); if (subtype == null) { - return; + return null; } final EditorInfo editorInfo = new EditorInfo(); editorInfo.inputType = InputType.TYPE_CLASS_TEXT; @@ -131,59 +145,41 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr final int keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res); final int keyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res); builder.setKeyboardGeometry(keyboardWidth, keyboardHeight); - builder.setSubtype(subtype); + builder.setSubtype(new RichInputMethodSubtype(subtype)); builder.setIsSpellChecker(false /* isSpellChecker */); final KeyboardLayoutSet layoutSet = builder.build(); - mKeyboard = layoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); - } - - private void loadDictionariesForLocale(final Locale newlocale) throws InterruptedException { - mDictionaryFacilitator.resetDictionaries(mContext, newlocale, - false /* useContactsDict */, false /* usePersonalizedDicts */, - false /* forceReloadMainDictionary */, null /* listener */); - mDictionaryFacilitator.waitForLoadingMainDictionary( - TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS, TimeUnit.SECONDS); + final Keyboard newKeyboard = layoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); + mLocaleToKeyboardCache.put(locale, newKeyboard); + return newKeyboard; } /** * Determine whether a word is a distracter to words in dictionaries. * - * @param prevWordsInfo the information of previous words. Not used for now. + * @param ngramContext the n-gram context. Not used for now. * @param testedWord the word that will be tested to see whether it is a distracter to words * in dictionaries. * @param locale the locale of word. * @return true if testedWord is a distracter, otherwise false. */ @Override - public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo, + public boolean isDistracterToWordsInDictionaries(final NgramContext ngramContext, final String testedWord, final Locale locale) { if (locale == null) { return false; } - if (!locale.equals(mDictionaryFacilitator.getLocale())) { - synchronized (mLock) { - if (!mLocaleToSubtypeMap.containsKey(locale)) { - Log.e(TAG, "Locale " + locale + " is not enabled."); - // TODO: Investigate what we should do for disabled locales. - return false; - } - loadKeyboardForLocale(locale); - // Reset dictionaries for the locale. - try { - mDistractersCache.evictAll(); - loadDictionariesForLocale(locale); - } catch (final InterruptedException e) { - Log.e(TAG, "Interrupted while waiting for loading dicts in DistracterFilter", - e); - return false; - } - } + if (!mLocaleToSubtypeCache.containsKey(locale)) { + Log.e(TAG, "Locale " + locale + " is not enabled."); + // TODO: Investigate what we should do for disabled locales. + return false; } - + final DictionaryFacilitator dictionaryFacilitator = + mDictionaryFacilitatorLruCache.get(locale); if (DEBUG) { Log.d(TAG, "testedWord: " + testedWord); } - final Boolean isCachedDistracter = mDistractersCache.get(testedWord); + final Pair<Locale, String> cacheKey = new Pair<>(locale, testedWord); + final Boolean isCachedDistracter = mDistractersCache.get(cacheKey); if (isCachedDistracter != null && isCachedDistracter) { if (DEBUG) { Log.d(TAG, "isDistracter: true (cache hit)"); @@ -192,37 +188,38 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr } final boolean isDistracterCheckedByGetMaxFreqencyOfExactMatches = - checkDistracterUsingMaxFreqencyOfExactMatches(testedWord); + checkDistracterUsingMaxFreqencyOfExactMatches(dictionaryFacilitator, testedWord); if (isDistracterCheckedByGetMaxFreqencyOfExactMatches) { - // Add the word to the cache. - mDistractersCache.put(testedWord, Boolean.TRUE); + // Add the pair of locale and word to the cache. + mDistractersCache.put(cacheKey, Boolean.TRUE); return true; } - final boolean isValidWord = mDictionaryFacilitator.isValidWord(testedWord, - false /* ignoreCase */); - if (isValidWord) { - // Valid word is not a distractor. + final boolean Word = dictionaryFacilitator.isValidWord(testedWord, false /* ignoreCase */); + if (Word) { + // Valid word is not a distracter. if (DEBUG) { Log.d(TAG, "isDistracter: false (valid word)"); } return false; } + final Keyboard keyboard = getKeyboardForLocale(locale); final boolean isDistracterCheckedByGetSuggestion = - checkDistracterUsingGetSuggestions(testedWord); + checkDistracterUsingGetSuggestions(dictionaryFacilitator, keyboard, testedWord); if (isDistracterCheckedByGetSuggestion) { - // Add the word to the cache. - mDistractersCache.put(testedWord, Boolean.TRUE); + // Add the pair of locale and word to the cache. + mDistractersCache.put(cacheKey, Boolean.TRUE); return true; } return false; } - private boolean checkDistracterUsingMaxFreqencyOfExactMatches(final String testedWord) { + private static boolean checkDistracterUsingMaxFreqencyOfExactMatches( + final DictionaryFacilitator dictionaryFacilitator, final String testedWord) { // The tested word is a distracter when there is a word that is exact matched to the tested // word and its probability is higher than the tested word's probability. - final int perfectMatchFreq = mDictionaryFacilitator.getFrequency(testedWord); - final int exactMatchFreq = mDictionaryFacilitator.getMaxFrequencyOfExactMatches(testedWord); + final int perfectMatchFreq = dictionaryFacilitator.getFrequency(testedWord); + final int exactMatchFreq = dictionaryFacilitator.getMaxFrequencyOfExactMatches(testedWord); final boolean isDistracter = perfectMatchFreq < exactMatchFreq; if (DEBUG) { Log.d(TAG, "perfectMatchFreq: " + perfectMatchFreq); @@ -232,8 +229,10 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr return isDistracter; } - private boolean checkDistracterUsingGetSuggestions(final String testedWord) { - if (mKeyboard == null) { + private boolean checkDistracterUsingGetSuggestions( + final DictionaryFacilitator dictionaryFacilitator, final Keyboard keyboard, + final String testedWord) { + if (keyboard == null) { return false; } final SettingsValuesForSuggestion settingsValuesForSuggestion = @@ -246,24 +245,24 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr testedWord; final WordComposer composer = new WordComposer(); final int[] codePoints = StringUtils.toCodePointArray(testedWord); - + final int[] coordinates = keyboard.getCoordinates(codePoints); + composer.setComposingWord(codePoints, coordinates); + final SuggestionResults suggestionResults; synchronized (mLock) { - final int[] coordinates = mKeyboard.getCoordinates(codePoints); - composer.setComposingWord(codePoints, coordinates); - final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( - composer, PrevWordsInfo.EMPTY_PREV_WORDS_INFO, mKeyboard.getProximityInfo(), + suggestionResults = dictionaryFacilitator.getSuggestionResults( + composer, NgramContext.EMPTY_PREV_WORDS_INFO, keyboard.getProximityInfo(), settingsValuesForSuggestion, 0 /* sessionId */); - if (suggestionResults.isEmpty()) { - return false; - } - final SuggestedWordInfo firstSuggestion = suggestionResults.first(); - final boolean isDistractor = suggestionExceedsDistracterThreshold( - firstSuggestion, consideredWord, DISTRACTER_WORD_SCORE_THRESHOLD); - if (DEBUG) { - Log.d(TAG, "isDistracter: " + isDistractor); - } - return isDistractor; } + if (suggestionResults.isEmpty()) { + return false; + } + final SuggestedWordInfo firstSuggestion = suggestionResults.first(); + final boolean isDistracter = suggestionExceedsDistracterThreshold( + firstSuggestion, consideredWord, DISTRACTER_WORD_SCORE_THRESHOLD); + if (DEBUG) { + Log.d(TAG, "isDistracter: " + isDistracter); + } + return isDistracter; } private static boolean suggestionExceedsDistracterThreshold(final SuggestedWordInfo suggestion, @@ -283,4 +282,41 @@ public class DistracterFilterCheckingExactMatchesAndSuggestions implements Distr } return false; } + + private boolean shouldBeLowerCased(final NgramContext ngramContext, final String testedWord, + final Locale locale) { + final DictionaryFacilitator dictionaryFacilitator = + mDictionaryFacilitatorLruCache.get(locale); + if (dictionaryFacilitator.isValidWord(testedWord, false /* ignoreCase */)) { + return false; + } + final String lowerCaseTargetWord = testedWord.toLowerCase(locale); + if (testedWord.equals(lowerCaseTargetWord)) { + return false; + } + if (dictionaryFacilitator.isValidWord(lowerCaseTargetWord, false /* ignoreCase */)) { + return true; + } + if (StringUtils.getCapitalizationType(testedWord) == StringUtils.CAPITALIZE_FIRST + && !ngramContext.isValid()) { + // TODO: Check beginning-of-sentence. + return true; + } + return false; + } + + @Override + public int getWordHandlingType(final NgramContext ngramContext, final String testedWord, + final Locale locale) { + // TODO: Use this method for user history dictionary. + if (testedWord == null|| locale == null) { + return HandlingType.getHandlingType(false /* shouldBeLowerCased */, false /* isOov */); + } + final boolean shouldBeLowerCased = shouldBeLowerCased(ngramContext, testedWord, locale); + final String caseModifiedWord = + shouldBeLowerCased ? testedWord.toLowerCase(locale) : testedWord; + final boolean isOov = !mDictionaryFacilitatorLruCache.get(locale).isValidWord( + caseModifiedWord, false /* ignoreCase */); + return HandlingType.getHandlingType(shouldBeLowerCased, isOov); + } } diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java index 4ad4ba784..df6e97028 100644 --- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java +++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java @@ -22,7 +22,7 @@ import java.util.Locale; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.NgramContext; public class DistracterFilterCheckingIsInDictionary implements DistracterFilter { private final DistracterFilter mDistracterFilter; @@ -35,7 +35,7 @@ public class DistracterFilterCheckingIsInDictionary implements DistracterFilter } @Override - public boolean isDistracterToWordsInDictionaries(PrevWordsInfo prevWordsInfo, + public boolean isDistracterToWordsInDictionaries(NgramContext ngramContext, String testedWord, Locale locale) { if (mDictionary.isInDictionary(testedWord)) { // This filter treats entries that are already in the dictionary as non-distracters @@ -43,11 +43,17 @@ public class DistracterFilterCheckingIsInDictionary implements DistracterFilter return false; } else { return mDistracterFilter.isDistracterToWordsInDictionaries( - prevWordsInfo, testedWord, locale); + ngramContext, testedWord, locale); } } @Override + public int getWordHandlingType(final NgramContext ngramContext, final String testedWord, + final Locale locale) { + return mDistracterFilter.getWordHandlingType(ngramContext, testedWord, locale); + } + + @Override public void updateEnabledSubtypes(List<InputMethodSubtype> enabledSubtypes) { // Do nothing. } diff --git a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java index c2167a76b..ae2de44c7 100644 --- a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin.utils; import com.android.inputmethod.dictionarypack.DictionarySettingsFragment; import com.android.inputmethod.latin.about.AboutPreferences; +import com.android.inputmethod.latin.settings.AccountsSettingsFragment; import com.android.inputmethod.latin.settings.AdvancedSettingsFragment; import com.android.inputmethod.latin.settings.AppearanceSettingsFragment; import com.android.inputmethod.latin.settings.CorrectionSettingsFragment; @@ -42,6 +43,7 @@ public class FragmentUtils { sLatinImeFragments.add(DictionarySettingsFragment.class.getName()); sLatinImeFragments.add(AboutPreferences.class.getName()); sLatinImeFragments.add(PreferencesSettingsFragment.class.getName()); + sLatinImeFragments.add(AccountsSettingsFragment.class.getName()); sLatinImeFragments.add(AppearanceSettingsFragment.class.getName()); sLatinImeFragments.add(ThemeSettingsFragment.class.getName()); sLatinImeFragments.add(MultiLingualSettingsFragment.class.getName()); diff --git a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java index ea406fa75..142548b25 100644 --- a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java @@ -53,7 +53,8 @@ public final class ImportantNoticeUtils { // This utility class is not publicly instantiable. } - private static boolean isInSystemSetupWizard(final Context context) { + @UsedForTesting + static boolean isInSystemSetupWizard(final Context context) { try { final int userSetupComplete = Settings.Secure.getInt( context.getContentResolver(), Settings_Secure_USER_SETUP_COMPLETE); @@ -84,7 +85,8 @@ public final class ImportantNoticeUtils { return getLastImportantNoticeVersion(context) + 1; } - private static boolean hasNewImportantNotice(final Context context) { + @UsedForTesting + static boolean hasNewImportantNotice(final Context context) { final int lastVersion = getLastImportantNoticeVersion(context); return getCurrentImportantNoticeVersion(context) > lastVersion; } diff --git a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java index fbce3f2fd..73aefb821 100644 --- a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java +++ b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java @@ -18,10 +18,12 @@ package com.android.inputmethod.latin.utils; import android.util.Log; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.DictionaryFacilitator; -import com.android.inputmethod.latin.PrevWordsInfo; +import com.android.inputmethod.latin.NgramContext; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; +import com.android.inputmethod.latin.utils.DistracterFilter.HandlingType; import java.util.ArrayList; import java.util.List; @@ -57,12 +59,14 @@ public final class LanguageModelParam { public final int mTimestamp; // Constructor for unigram. TODO: support shortcuts + @UsedForTesting public LanguageModelParam(final CharSequence word, final int unigramProbability, final int timestamp) { this(null /* word0 */, word, unigramProbability, Dictionary.NOT_A_PROBABILITY, timestamp); } // Constructor for unigram and bigram. + @UsedForTesting public LanguageModelParam(final CharSequence word0, final CharSequence word1, final int unigramProbability, final int bigramProbability, final int timestamp) { @@ -81,12 +85,11 @@ public final class LanguageModelParam { // Process a list of words and return a list of {@link LanguageModelParam} objects. public static ArrayList<LanguageModelParam> createLanguageModelParamsFrom( final List<String> tokens, final int timestamp, - final DictionaryFacilitator dictionaryFacilitator, - final SpacingAndPunctuations spacingAndPunctuations, + final SpacingAndPunctuations spacingAndPunctuations, final Locale locale, final DistracterFilter distracterFilter) { final ArrayList<LanguageModelParam> languageModelParams = new ArrayList<>(); final int N = tokens.size(); - PrevWordsInfo prevWordsInfo = PrevWordsInfo.EMPTY_PREV_WORDS_INFO; + NgramContext ngramContext = NgramContext.EMPTY_PREV_WORDS_INFO; for (int i = 0; i < N; ++i) { final String tempWord = tokens.get(i); if (StringUtils.isEmptyStringOrWhiteSpaces(tempWord)) { @@ -103,7 +106,7 @@ public final class LanguageModelParam { + tempWord + "\""); } // Sentence terminator found. Split. - prevWordsInfo = PrevWordsInfo.EMPTY_PREV_WORDS_INFO; + ngramContext = NgramContext.EMPTY_PREV_WORDS_INFO; continue; } if (DEBUG_TOKEN) { @@ -111,64 +114,41 @@ public final class LanguageModelParam { } final LanguageModelParam languageModelParam = detectWhetherVaildWordOrNotAndGetLanguageModelParam( - prevWordsInfo, tempWord, timestamp, dictionaryFacilitator, - distracterFilter); + ngramContext, tempWord, timestamp, locale, distracterFilter); if (languageModelParam == null) { continue; } languageModelParams.add(languageModelParam); - prevWordsInfo = prevWordsInfo.getNextPrevWordsInfo( - new PrevWordsInfo.WordInfo(tempWord)); + ngramContext = ngramContext.getNextNgramContext( + new NgramContext.WordInfo(tempWord)); } return languageModelParams; } private static LanguageModelParam detectWhetherVaildWordOrNotAndGetLanguageModelParam( - final PrevWordsInfo prevWordsInfo, final String targetWord, final int timestamp, - final DictionaryFacilitator dictionaryFacilitator, - final DistracterFilter distracterFilter) { - final Locale locale = dictionaryFacilitator.getLocale(); + final NgramContext ngramContext, final String targetWord, final int timestamp, + final Locale locale, final DistracterFilter distracterFilter) { if (locale == null) { return null; } - if (dictionaryFacilitator.isValidWord(targetWord, false /* ignoreCase */)) { - return createAndGetLanguageModelParamOfWord(prevWordsInfo, targetWord, timestamp, - true /* isValidWord */, locale, distracterFilter); - } - - final String lowerCaseTargetWord = targetWord.toLowerCase(locale); - if (dictionaryFacilitator.isValidWord(lowerCaseTargetWord, false /* ignoreCase */)) { - // Add the lower-cased word. - return createAndGetLanguageModelParamOfWord(prevWordsInfo, lowerCaseTargetWord, - timestamp, true /* isValidWord */, locale, distracterFilter); + final int wordHandlingType = distracterFilter.getWordHandlingType(ngramContext, + targetWord, locale); + final String word = HandlingType.shouldBeLowerCased(wordHandlingType) ? + targetWord.toLowerCase(locale) : targetWord; + if (distracterFilter.isDistracterToWordsInDictionaries(ngramContext, targetWord, locale)) { + // The word is a distracter. + return null; } - - // Treat the word as an OOV word. - return createAndGetLanguageModelParamOfWord(prevWordsInfo, targetWord, timestamp, - false /* isValidWord */, locale, distracterFilter); + return createAndGetLanguageModelParamOfWord(ngramContext, word, timestamp, + !HandlingType.shouldBeHandledAsOov(wordHandlingType)); } private static LanguageModelParam createAndGetLanguageModelParamOfWord( - final PrevWordsInfo prevWordsInfo, final String targetWord, final int timestamp, - final boolean isValidWord, final Locale locale, - final DistracterFilter distracterFilter) { - final String word; - if (StringUtils.getCapitalizationType(targetWord) == StringUtils.CAPITALIZE_FIRST - && !prevWordsInfo.isValid() && !isValidWord) { - word = targetWord.toLowerCase(locale); - } else { - word = targetWord; - } - // Check whether the word is a distracter to words in the dictionaries. - if (distracterFilter.isDistracterToWordsInDictionaries(prevWordsInfo, word, locale)) { - if (DEBUG) { - Log.d(TAG, "The word (" + word + ") is a distracter. Skip this word."); - } - return null; - } + final NgramContext ngramContext, final String word, final int timestamp, + final boolean isValidWord) { final int unigramProbability = isValidWord ? UNIGRAM_PROBABILITY_FOR_VALID_WORD : UNIGRAM_PROBABILITY_FOR_OOV_WORD; - if (!prevWordsInfo.isValid()) { + if (!ngramContext.isValid()) { if (DEBUG) { Log.d(TAG, "--- add unigram: current(" + (isValidWord ? "Valid" : "OOV") + ") = " + word); @@ -176,12 +156,12 @@ public final class LanguageModelParam { return new LanguageModelParam(word, unigramProbability, timestamp); } if (DEBUG) { - Log.d(TAG, "--- add bigram: prev = " + prevWordsInfo + ", current(" + Log.d(TAG, "--- add bigram: prev = " + ngramContext + ", current(" + (isValidWord ? "Valid" : "OOV") + ") = " + word); } final int bigramProbability = isValidWord ? BIGRAM_PROBABILITY_FOR_VALID_WORD : BIGRAM_PROBABILITY_FOR_OOV_WORD; - return new LanguageModelParam(prevWordsInfo.mPrevWordsInfo[0].mWord, word, + return new LanguageModelParam(ngramContext.getNthPrevWord(1 /* n */), word, unigramProbability, bigramProbability, timestamp); } } diff --git a/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java b/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java index dd6fac671..9a5be99b3 100644 --- a/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java +++ b/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java @@ -21,21 +21,22 @@ import android.os.Looper; import java.lang.ref.WeakReference; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + public class LeakGuardHandlerWrapper<T> extends Handler { private final WeakReference<T> mOwnerInstanceRef; - public LeakGuardHandlerWrapper(final T ownerInstance) { + public LeakGuardHandlerWrapper(@Nonnull final T ownerInstance) { this(ownerInstance, Looper.myLooper()); } - public LeakGuardHandlerWrapper(final T ownerInstance, final Looper looper) { + public LeakGuardHandlerWrapper(@Nonnull final T ownerInstance, final Looper looper) { super(looper); - if (ownerInstance == null) { - throw new NullPointerException("ownerInstance is null"); - } mOwnerInstanceRef = new WeakReference<>(ownerInstance); } + @Nullable public T getOwnerInstance() { return mOwnerInstanceRef.get(); } diff --git a/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/NgramContextUtils.java index 3cd63612c..34eeac2c2 100644 --- a/java/src/com/android/inputmethod/latin/utils/PrevWordsInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/NgramContextUtils.java @@ -16,15 +16,16 @@ package com.android.inputmethod.latin.utils; +import java.util.Arrays; import java.util.regex.Pattern; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.PrevWordsInfo; -import com.android.inputmethod.latin.PrevWordsInfo.WordInfo; +import com.android.inputmethod.latin.NgramContext; +import com.android.inputmethod.latin.NgramContext.WordInfo; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; -public final class PrevWordsInfoUtils { - private PrevWordsInfoUtils() { +public final class NgramContextUtils { + private NgramContextUtils() { // Intentional empty constructor for utility class. } @@ -43,7 +44,7 @@ public final class PrevWordsInfoUtils { // (n = 2) "abc def|" -> beginning-of-sentence, abc // (n = 2) "abc def |" -> beginning-of-sentence, abc // (n = 2) "abc 'def|" -> empty. The context is different from "abc def", but we cannot - // represent this situation using PrevWordsInfo. See TODO in the method. + // represent this situation using NgramContext. See TODO in the method. // TODO: The next example's result should be "abc, def". This have to be fixed before we // retrieve the prior context of Beginning-of-Sentence. // (n = 2) "abc def. |" -> beginning-of-sentence, abc @@ -51,11 +52,12 @@ public final class PrevWordsInfoUtils { // (n = 2) "abc|" -> beginning-of-sentence // (n = 2) "abc |" -> beginning-of-sentence // (n = 2) "abc. def|" -> beginning-of-sentence - public static PrevWordsInfo getPrevWordsInfoFromNthPreviousWord(final CharSequence prev, + public static NgramContext getNgramContextFromNthPreviousWord(final CharSequence prev, final SpacingAndPunctuations spacingAndPunctuations, final int n) { - if (prev == null) return PrevWordsInfo.EMPTY_PREV_WORDS_INFO; + if (prev == null) return NgramContext.EMPTY_PREV_WORDS_INFO; final String[] w = SPACE_REGEX.split(prev); final WordInfo[] prevWordsInfo = new WordInfo[Constants.MAX_PREV_WORD_COUNT_FOR_N_GRAM]; + Arrays.fill(prevWordsInfo, WordInfo.EMPTY_WORD_INFO); for (int i = 0; i < prevWordsInfo.length; i++) { final int focusedWordIndex = w.length - n - i; // Referring to the word after the focused word. @@ -66,7 +68,6 @@ public final class PrevWordsInfoUtils { if (spacingAndPunctuations.isWordConnector(firstChar)) { // The word following the focused word is starting with a word connector. // TODO: Return meaningful context for this case. - prevWordsInfo[i] = WordInfo.EMPTY_WORD_INFO; break; } } @@ -93,11 +94,10 @@ public final class PrevWordsInfoUtils { // TODO: Return meaningful context for this case. if (spacingAndPunctuations.isWordSeparator(lastChar) || spacingAndPunctuations.isWordConnector(lastChar)) { - prevWordsInfo[i] = WordInfo.EMPTY_WORD_INFO; break; } prevWordsInfo[i] = new WordInfo(focusedWord); } - return new PrevWordsInfo(prevWordsInfo); + return new NgramContext(prevWordsInfo); } } diff --git a/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java b/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java deleted file mode 100644 index 1ca895fdb..000000000 --- a/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2014 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.utils; - -import android.view.inputmethod.InputMethodSubtype; - -public final class SpacebarLanguageUtils { - private SpacebarLanguageUtils() { - // Intentional empty constructor for utility class. - } - - // InputMethodSubtype's display name for spacebar text in its locale. - // isAdditionalSubtype (T=true, F=false) - // locale layout | Middle Full - // ------ ------- - --------- ---------------------- - // en_US qwerty F English English (US) exception - // en_GB qwerty F English English (UK) exception - // es_US spanish F Español Español (EE.UU.) exception - // fr azerty F Français Français - // fr_CA qwerty F Français Français (Canada) - // fr_CH swiss F Français Français (Suisse) - // de qwertz F Deutsch Deutsch - // de_CH swiss T Deutsch Deutsch (Schweiz) - // zz qwerty F QWERTY QWERTY - // fr qwertz T Français Français - // de qwerty T Deutsch Deutsch - // en_US azerty T English English (US) - // zz azerty T AZERTY AZERTY - // Get InputMethodSubtype's full display name in its locale. - public static String getFullDisplayName(final InputMethodSubtype subtype) { - if (SubtypeLocaleUtils.isNoLanguage(subtype)) { - return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype); - } - return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(subtype.getLocale()); - } - - // Get InputMethodSubtype's middle display name in its locale. - public static String getMiddleDisplayName(final InputMethodSubtype subtype) { - if (SubtypeLocaleUtils.isNoLanguage(subtype)) { - return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype); - } - return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(subtype.getLocale()); - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java index 1781924ac..bbcef990d 100644 --- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java @@ -49,9 +49,9 @@ public final class StringUtils { // This utility class is not publicly instantiable. } - public static int codePointCount(final String text) { + public static int codePointCount(final CharSequence text) { if (TextUtils.isEmpty(text)) return 0; - return text.codePointCount(0, text.length()); + return Character.codePointCount(text, 0, text.length()); } public static String newSingleCodePointString(int codePoint) { diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java index 351d01400..61661cd52 100644 --- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java @@ -27,11 +27,17 @@ import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.RichInputMethodSubtype; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Locale; +/** + * A helper class to deal with subtype locales. + */ +// TODO: consolidate this into RichInputMethodSubtype public final class SubtypeLocaleUtils { private static final String TAG = SubtypeLocaleUtils.class.getSimpleName(); @@ -52,6 +58,8 @@ public final class SubtypeLocaleUtils { private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap = new HashMap<>(); // Keyboard layout to subtype name resource id map. private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap = new HashMap<>(); + // Exceptional locale whose name should be displayed in Locale.ROOT. + static final HashSet<String> sExceptionalLocaleDisplayedInRootLocale = new HashSet<>(); // Exceptional locale to subtype name resource id map. private static final HashMap<String, Integer> sExceptionalLocaleToNameIdsMap = new HashMap<>(); // Exceptional locale to subtype name with layout resource id map. @@ -106,6 +114,12 @@ public final class SubtypeLocaleUtils { sKeyboardLayoutToNameIdsMap.put(key, noLanguageResId); } + final String[] exceptionalLocaleInRootLocale = res.getStringArray( + R.array.subtype_locale_displayed_in_root_locale); + for (int i = 0; i < exceptionalLocaleInRootLocale.length; i++) { + sExceptionalLocaleDisplayedInRootLocale.add(exceptionalLocaleInRootLocale[i]); + } + final String[] exceptionalLocales = res.getStringArray( R.array.subtype_locale_exception_keys); for (int i = 0; i < exceptionalLocales.length; i++) { @@ -153,10 +167,13 @@ public final class SubtypeLocaleUtils { return nameId == null ? UNKNOWN_KEYBOARD_LAYOUT : nameId; } - private static Locale getDisplayLocaleOfSubtypeLocale(final String localeString) { + public static Locale getDisplayLocaleOfSubtypeLocale(final String localeString) { if (NO_LANGUAGE.equals(localeString)) { return sResources.getConfiguration().locale; } + if (sExceptionalLocaleDisplayedInRootLocale.contains(localeString)) { + return Locale.ROOT; + } return LocaleUtils.constructLocaleFromString(localeString); } @@ -171,9 +188,15 @@ public final class SubtypeLocaleUtils { } public static String getSubtypeLanguageDisplayName(final String localeString) { - final Locale locale = LocaleUtils.constructLocaleFromString(localeString); final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString); - return getSubtypeLocaleDisplayNameInternal(locale.getLanguage(), displayLocale); + final String languageString; + if (sExceptionalLocaleDisplayedInRootLocale.contains(localeString)) { + languageString = localeString; + } else { + final Locale locale = LocaleUtils.constructLocaleFromString(localeString); + languageString = locale.getLanguage(); + } + return getSubtypeLocaleDisplayNameInternal(languageString, displayLocale); } private static String getSubtypeLocaleDisplayNameInternal(final String localeString, @@ -242,6 +265,7 @@ public final class SubtypeLocaleUtils { private static String getSubtypeDisplayNameInternal(final InputMethodSubtype subtype, final Locale displayLocale) { final String replacementString = getReplacementString(subtype, displayLocale); + // TODO: rework this for multi-lingual subtypes final int nameResId = subtype.getNameResId(); final RunInLocale<String> getSubtypeName = new RunInLocale<String>() { @Override @@ -264,11 +288,6 @@ public final class SubtypeLocaleUtils { getSubtypeName.runInLocale(sResources, displayLocale), displayLocale); } - public static boolean isNoLanguage(final InputMethodSubtype subtype) { - final String localeString = subtype.getLocale(); - return NO_LANGUAGE.equals(localeString); - } - public static Locale getSubtypeLocale(final InputMethodSubtype subtype) { final String localeString = subtype.getLocale(); return LocaleUtils.constructLocaleFromString(localeString); @@ -283,6 +302,10 @@ public final class SubtypeLocaleUtils { return sKeyboardLayoutToDisplayNameMap.get(layoutName); } + public static String getKeyboardLayoutSetName(final RichInputMethodSubtype subtype) { + return getKeyboardLayoutSetName(subtype.getRawSubtype()); + } + public static String getKeyboardLayoutSetName(final InputMethodSubtype subtype) { String keyboardLayoutSet = subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET); if (keyboardLayoutSet == null) { @@ -318,10 +341,6 @@ public final class SubtypeLocaleUtils { return Arrays.binarySearch(SORTED_RTL_LANGUAGES, language) >= 0; } - public static boolean isRtlLanguage(final InputMethodSubtype subtype) { - return isRtlLanguage(getSubtypeLocale(subtype)); - } - public static String getCombiningRulesExtraValue(final InputMethodSubtype subtype) { return subtype.getExtraValueOf(Constants.Subtype.ExtraValue.COMBINING_RULES); } diff --git a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java index 8cd49509f..4e2e396c2 100644 --- a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java +++ b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java @@ -22,7 +22,6 @@ import com.android.inputmethod.latin.define.ProductionFlags; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; -import java.util.Locale; import java.util.TreeSet; /** @@ -30,22 +29,19 @@ import java.util.TreeSet; * than its limit */ public final class SuggestionResults extends TreeSet<SuggestedWordInfo> { - public final Locale mLocale; public final ArrayList<SuggestedWordInfo> mRawSuggestions; // TODO: Instead of a boolean , we may want to include the context of this suggestion results, - // such as {@link PrevWordsInfo}. + // such as {@link NgramContext}. public final boolean mIsBeginningOfSentence; private final int mCapacity; - public SuggestionResults(final Locale locale, final int capacity, - final boolean isBeginningOfSentence) { - this(locale, sSuggestedWordInfoComparator, capacity, isBeginningOfSentence); + public SuggestionResults(final int capacity, final boolean isBeginningOfSentence) { + this(sSuggestedWordInfoComparator, capacity, isBeginningOfSentence); } - private SuggestionResults(final Locale locale, final Comparator<SuggestedWordInfo> comparator, + private SuggestionResults(final Comparator<SuggestedWordInfo> comparator, final int capacity, final boolean isBeginningOfSentence) { super(comparator); - mLocale = locale; mCapacity = capacity; if (ProductionFlags.INCLUDE_RAW_SUGGESTIONS) { mRawSuggestions = new ArrayList<>(); |