diff options
Diffstat (limited to 'java/src')
217 files changed, 17239 insertions, 19804 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java index 10fb9fef4..216a825e0 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java @@ -158,7 +158,7 @@ public final class AccessibilityUtils { * @param typedWord the currently typed word */ public void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) { - if (suggestedWords != null && suggestedWords.mWillAutoCorrect) { + if (suggestedWords.mWillAutoCorrect) { mAutoCorrectionWord = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION); mTypedWord = typedWord; } else { diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java index 73896dfd3..0043b7844 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java @@ -29,10 +29,10 @@ import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.KeyDetector; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.keyboard.MainKeyboardView; -import com.android.inputmethod.keyboard.PointerTracker; import com.android.inputmethod.latin.R; public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { @@ -82,7 +82,7 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp private void initInternal(final InputMethodService inputMethod) { mInputMethod = inputMethod; mEdgeSlop = inputMethod.getResources().getDimensionPixelSize( - R.dimen.accessibility_edge_slop); + R.dimen.config_accessibility_edge_slop); } /** @@ -204,25 +204,14 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp } /** - * Intercepts touch events before dispatch when touch exploration is turned on in ICS and - * higher. - * - * @param event The motion event being dispatched. - * @return {@code true} if the event is handled - */ - public boolean dispatchTouchEvent(final MotionEvent event) { - // To avoid accidental key presses during touch exploration, always drop - // touch events generated by the user. - return false; - } - - /** * Receives hover events when touch exploration is turned on in SDK versions ICS and higher. * * @param event The hover event. + * @param keyDetector The {@link KeyDetector} to determine on which key the <code>event</code> + * is hovering. * @return {@code true} if the event is handled */ - public boolean dispatchHoverEvent(final MotionEvent event, final PointerTracker tracker) { + public boolean dispatchHoverEvent(final MotionEvent event, final KeyDetector keyDetector) { if (mView == null) { return false; } @@ -233,7 +222,7 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp final Key key; if (pointInView(x, y)) { - key = tracker.getKeyOn(x, y); + key = keyDetector.detectHitKey(x, y); } else { key = null; } diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java index 58624a2e6..2e6649bf2 100644 --- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java +++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java @@ -58,9 +58,6 @@ public final class KeyCodeDescriptionMapper { } private void initInternal() { - // Manual label substitutions for key labels with no string resource - mKeyLabelMap.put(":-)", R.string.spoken_description_smiley); - // Special non-character codes defined in Keyboard mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space); mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete); @@ -75,6 +72,7 @@ public final class KeyCodeDescriptionMapper { mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next); mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS, R.string.spoken_description_action_previous); + mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji); } /** diff --git a/java/src/com/android/inputmethod/compat/AppWorkaroundsUtils.java b/java/src/com/android/inputmethod/compat/AppWorkaroundsUtils.java index 7e9e2e37b..6e43cc9a7 100644 --- a/java/src/com/android/inputmethod/compat/AppWorkaroundsUtils.java +++ b/java/src/com/android/inputmethod/compat/AppWorkaroundsUtils.java @@ -23,10 +23,10 @@ import android.os.Build.VERSION_CODES; * A class to encapsulate work-arounds specific to particular apps. */ public class AppWorkaroundsUtils { - private PackageInfo mPackageInfo; // May be null - private boolean mIsBrokenByRecorrection = false; + private final PackageInfo mPackageInfo; // May be null + private final boolean mIsBrokenByRecorrection; - public void setPackageInfo(final PackageInfo packageInfo) { + public AppWorkaroundsUtils(final PackageInfo packageInfo) { mPackageInfo = packageInfo; mIsBrokenByRecorrection = AppWorkaroundsHelper.evaluateIsBrokenByRecorrection( packageInfo); diff --git a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java index 14ee654f3..81df17127 100644 --- a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java @@ -17,11 +17,12 @@ package com.android.inputmethod.compat; import android.inputmethodservice.InputMethodService; +import com.android.inputmethod.latin.define.ProductionFlag; import java.lang.reflect.Method; public final class InputMethodServiceCompatUtils { - // Note that InputMethodService.enableHardwareAcceleration() has been introduced + // Note that {@link InputMethodService#enableHardwareAcceleration} has been introduced // in API level 17 (Build.VERSION_CODES.JELLY_BEAN_MR1). private static final Method METHOD_enableHardwareAcceleration = CompatUtils.getMethod(InputMethodService.class, "enableHardwareAcceleration"); @@ -34,4 +35,30 @@ public final class InputMethodServiceCompatUtils { return (Boolean)CompatUtils.invoke(ims, false /* defaultValue */, METHOD_enableHardwareAcceleration); } + + public static void setCursorAnchorMonitorMode(final InputMethodService ims, final int mode) { + if (ProductionFlag.USES_CURSOR_ANCHOR_MONITOR) { + ExperimentalAPIUtils.setCursorAnchorMonitorMode(ims, mode); + } + } + + /* + * For unreleased APIs. ProGuard will strip this class entirely, unless used explicitly. + */ + private static final class ExperimentalAPIUtils { + // Note that {@link InputMethodManager#setCursorAnchorMonitorMode} is not yet available as + // an official API as of API level 19 (Build.VERSION_CODES.KITKAT). + private static final Method METHOD_setCursorAnchorMonitorMode = CompatUtils.getMethod( + InputMethodService.class, "setCursorAnchorMonitorMode", int.class); + + private ExperimentalAPIUtils() { + // This utility class is not publicly instantiable. + } + + public static void setCursorAnchorMonitorMode(final InputMethodService ims, + final int mode) { + CompatUtils.invoke(ims, null /* defaultValue */, + METHOD_setCursorAnchorMonitorMode, mode); + } + } } diff --git a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java index b119d6c82..4ea7fb888 100644 --- a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java @@ -19,7 +19,10 @@ package com.android.inputmethod.compat; import android.os.Build; import android.view.inputmethod.InputMethodSubtype; +import com.android.inputmethod.latin.Constants; + import java.lang.reflect.Constructor; +import java.lang.reflect.Method; public final class InputMethodSubtypeCompatUtils { private static final String TAG = InputMethodSubtypeCompatUtils.class.getSimpleName(); @@ -37,6 +40,12 @@ public final class InputMethodSubtypeCompatUtils { } } } + + // Note that {@link InputMethodSubtype#isAsciiCapable()} has been introduced in API level 19 + // (Build.VERSION_CODE.KITKAT). + private static final Method METHOD_isAsciiCapable = CompatUtils.getMethod( + InputMethodSubtype.class, "isAsciiCapable"); + private InputMethodSubtypeCompatUtils() { // This utility class is not publicly instantiable. } @@ -53,4 +62,9 @@ public final class InputMethodSubtypeCompatUtils { nameId, iconId, locale, mode, extraValue, isAuxiliary, overridesImplicitlyEnabledSubtype, id); } + + public static boolean isAsciiCapable(final InputMethodSubtype subtype) { + return (Boolean)CompatUtils.invoke(subtype, false, METHOD_isAsciiCapable) + || subtype.containsExtraValueKey(Constants.Subtype.ExtraValue.ASCII_CAPABLE); + } } diff --git a/java/src/com/android/inputmethod/compat/LooperCompatUtils.java b/java/src/com/android/inputmethod/compat/LooperCompatUtils.java new file mode 100644 index 000000000..d647dbbd3 --- /dev/null +++ b/java/src/com/android/inputmethod/compat/LooperCompatUtils.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.os.Looper; + +import java.lang.reflect.Method; + +/** + * Helper to call Looper#quitSafely, which was introduced in API + * level 18 (Build.VERSION_CODES.JELLY_BEAN_MR2). + * + * In unit tests, we create lots of instances of LatinIME, which means we need to clean up + * some Loopers lest we leak file descriptors. In normal use on a device though, this is never + * necessary (although it does not hurt). + */ +public final class LooperCompatUtils { + private static final Method METHOD_quitSafely = CompatUtils.getMethod( + Looper.class, "quitSafely"); + + public static void quitSafely(final Looper looper) { + if (null != METHOD_quitSafely) { + CompatUtils.invoke(looper, null /* default return value */, METHOD_quitSafely); + } else { + looper.quit(); + } + } +} diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java index 55282c583..60f7e2def 100644 --- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java +++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java @@ -25,6 +25,7 @@ import android.text.style.SuggestionSpan; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver; import com.android.inputmethod.latin.utils.CollectionUtils; @@ -66,30 +67,30 @@ public final class SuggestionSpanUtils { } public static CharSequence getTextWithSuggestionSpan(final Context context, - final String pickedWord, final SuggestedWords suggestedWords, - final boolean dictionaryAvailable) { - if (!dictionaryAvailable || TextUtils.isEmpty(pickedWord) || suggestedWords.isEmpty() - || suggestedWords.mIsPrediction || suggestedWords.mIsPunctuationSuggestions) { + final String pickedWord, final SuggestedWords suggestedWords) { + if (TextUtils.isEmpty(pickedWord) || suggestedWords.isEmpty() + || suggestedWords.mIsPrediction || suggestedWords.isPunctuationSuggestions()) { return pickedWord; } - final Spannable spannable = new SpannableString(pickedWord); final ArrayList<String> suggestionsList = CollectionUtils.newArrayList(); for (int i = 0; i < suggestedWords.size(); ++i) { if (suggestionsList.size() >= SuggestionSpan.SUGGESTIONS_MAX_SIZE) { break; } + final SuggestedWordInfo info = suggestedWords.getInfo(i); + if (info.mKind == SuggestedWordInfo.KIND_PREDICTION) { + continue; + } final String word = suggestedWords.getWord(i); if (!TextUtils.equals(pickedWord, word)) { suggestionsList.add(word.toString()); } } - - // TODO: We should avoid adding suggestion span candidates that came from the bigram - // prediction. final SuggestionSpan suggestionSpan = new SuggestionSpan(context, null /* locale */, suggestionsList.toArray(new String[suggestionsList.size()]), 0 /* flags */, SuggestionSpanPickedNotificationReceiver.class); + final Spannable spannable = new SpannableString(pickedWord); spannable.setSpan(suggestionSpan, 0, pickedWord.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return spannable; } diff --git a/java/src/com/android/inputmethod/compat/ViewCompatUtils.java b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java index a8fab8855..dec739d39 100644 --- a/java/src/com/android/inputmethod/compat/ViewCompatUtils.java +++ b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java @@ -20,6 +20,9 @@ import android.view.View; import java.lang.reflect.Method; +// TODO: Use {@link android.support.v4.view.ViewCompat} instead of this utility class. +// Currently {@link #getPaddingEnd(View)} and {@link #setPaddingRelative(View,int,int,int,int)} +// are missing from android-support-v4 static library in KitKat SDK. public final class ViewCompatUtils { // Note that View.LAYOUT_DIRECTION_LTR and View.LAYOUT_DIRECTION_RTL have been introduced in // API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1). diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java index d5e638e7e..706bdea8e 100644 --- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java +++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java @@ -117,16 +117,11 @@ public final class ActionBatch { final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, mWordList.mId, mWordList.mVersion); final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); - final DownloadManager manager = - (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); if (MetadataDbHelper.STATUS_DOWNLOADING == status) { // The word list is still downloading. Cancel the download and revert the // word list status to "available". - if (null != manager) { - // DownloadManager is disabled (or not installed?). We can't cancel - there - // is nothing we can do. We still need to mark the entry as 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) { // Should never happen @@ -136,9 +131,6 @@ public final class ActionBatch { // Download it. DebugLogUtils.l("Upgrade word list, downloading", mWordList.mRemoteFilename); - // TODO: if DownloadManager is disabled or not installed, download by ourselves - if (null == manager) return; - // This is an upgraded word list: we should download it. // Adding a disambiguator to circumvent a bug in older versions of DownloadManager. // DownloadManager also stupidly cuts the extension to replace with its own that it @@ -293,13 +285,8 @@ public final class ActionBatch { } // The word list is still downloading. Cancel the download and revert the // word list status to "available". - final DownloadManager manager = - (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - if (null != manager) { - // If we can't cancel the download because DownloadManager is not available, - // we still need to mark the entry as available. - manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); - } + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); + manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN)); MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion); } } diff --git a/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java b/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java index 7c27e6d51..3d0e29ed0 100644 --- a/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java +++ b/java/src/com/android/inputmethod/dictionarypack/CommonPreferences.java @@ -23,7 +23,7 @@ public final class CommonPreferences { private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs"; public static SharedPreferences getCommonPreferences(final Context context) { - return context.getSharedPreferences(COMMON_PREFERENCES_NAME, Context.MODE_WORLD_READABLE); + return context.getSharedPreferences(COMMON_PREFERENCES_NAME, 0); } public static void enable(final SharedPreferences pref, final String id) { diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java index 88b5032e3..2623eff56 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java @@ -100,32 +100,29 @@ public class DictionaryDownloadProgressBar extends ProgressBar { @Override protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); mIsCurrentlyAttachedToWindow = false; updateReporterThreadRunningStatusAccordingToVisibility(); } private class UpdaterThread extends Thread { private final static int REPORT_PERIOD = 150; // how often to report progress, in ms - final DownloadManager mDownloadManager; + final DownloadManagerWrapper mDownloadManagerWrapper; final int mId; public UpdaterThread(final Context context, final int id) { super(); - mDownloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + mDownloadManagerWrapper = new DownloadManagerWrapper(context); mId = id; } @Override public void run() { try { - // It's almost impossible that mDownloadManager is null (it would mean it has been - // disabled between pressing the 'install' button and displaying the progress - // bar), but just in case. - if (null == mDownloadManager) return; final UpdateHelper updateHelper = new UpdateHelper(); final Query query = new Query().setFilterById(mId); int lastProgress = 0; setIndeterminate(true); while (!isInterrupted()) { - final Cursor cursor = mDownloadManager.query(query); + final Cursor cursor = mDownloadManagerWrapper.query(query); if (null == cursor) { // Can't contact DownloadManager: this should never happen. return; diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java index 1d9b9991e..80def701d 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java @@ -350,7 +350,8 @@ public final class DictionaryProvider extends ContentProvider { clientId); if (null == results) { return Collections.<WordListInfo>emptyList(); - } else { + } + try { final HashMap<String, WordListInfo> dicts = new HashMap<String, WordListInfo>(); final int idIndex = results.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN); final int localeIndex = results.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN); @@ -416,8 +417,9 @@ public final class DictionaryProvider extends ContentProvider { } } while (results.moveToNext()); } - results.close(); return Collections.unmodifiableCollection(dicts.values()); + } finally { + results.close(); } } diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java index 7bbd041e7..dae2f22a4 100644 --- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java +++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java @@ -283,59 +283,70 @@ public final class DictionarySettingsFragment extends PreferenceFragment final ArrayList<Preference> result = new ArrayList<Preference>(); result.add(createErrorMessage(activity, R.string.cannot_connect_to_dict_service)); return result; - } else if (!cursor.moveToFirst()) { - final ArrayList<Preference> result = new ArrayList<Preference>(); - result.add(createErrorMessage(activity, R.string.no_dictionaries_available)); - cursor.close(); - return result; - } else { - final String systemLocaleString = Locale.getDefault().toString(); - final TreeMap<String, WordListPreference> prefMap = - new TreeMap<String, WordListPreference>(); - final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN); - final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN); - final int localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN); - final int descriptionIndex = cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN); - final int statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN); - final int filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN); - do { - final String wordlistId = cursor.getString(idIndex); - final int version = cursor.getInt(versionIndex); - final String localeString = cursor.getString(localeIndex); - final Locale locale = new Locale(localeString); - final String description = cursor.getString(descriptionIndex); - final int status = cursor.getInt(statusIndex); - final int matchLevel = LocaleUtils.getMatchLevel(systemLocaleString, localeString); - final String matchLevelString = LocaleUtils.getMatchLevelSortedString(matchLevel); - final int filesize = cursor.getInt(filesizeIndex); - // The key is sorted in lexicographic order, according to the match level, then - // the description. - final String key = matchLevelString + "." + description + "." + wordlistId; - final WordListPreference existingPref = prefMap.get(key); - if (null == existingPref || existingPref.hasPriorityOver(status)) { - final WordListPreference oldPreference = mCurrentPreferenceMap.get(key); - final WordListPreference pref; - if (null != oldPreference - && oldPreference.mVersion == version - && oldPreference.mLocale.equals(locale)) { - // If the old preference has all the new attributes, reuse it. We test - // for version and locale because although attributes other than status - // need to be the same, others have been tested through the key of the - // map. Also, status may differ so we don't want to use #equals() here. - pref = oldPreference; - pref.setStatus(status); - } else { - // Otherwise, discard it and create a new one instead. - pref = new WordListPreference(activity, mDictionaryListInterfaceState, - mClientId, wordlistId, version, locale, description, status, - filesize); + } + try { + if (!cursor.moveToFirst()) { + final ArrayList<Preference> result = new ArrayList<Preference>(); + result.add(createErrorMessage(activity, R.string.no_dictionaries_available)); + return result; + } else { + final String systemLocaleString = Locale.getDefault().toString(); + final TreeMap<String, WordListPreference> prefMap = + new TreeMap<String, WordListPreference>(); + final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN); + final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN); + final int localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN); + final int descriptionIndex = + cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN); + final int statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN); + final int filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN); + do { + final String wordlistId = cursor.getString(idIndex); + final int version = cursor.getInt(versionIndex); + final String localeString = cursor.getString(localeIndex); + final Locale locale = new Locale(localeString); + final String description = cursor.getString(descriptionIndex); + final int status = cursor.getInt(statusIndex); + final int matchLevel = + LocaleUtils.getMatchLevel(systemLocaleString, localeString); + final String matchLevelString = + LocaleUtils.getMatchLevelSortedString(matchLevel); + final int filesize = cursor.getInt(filesizeIndex); + // The key is sorted in lexicographic order, according to the match level, then + // the description. + final String key = matchLevelString + "." + description + "." + wordlistId; + final WordListPreference existingPref = prefMap.get(key); + if (null == existingPref || existingPref.hasPriorityOver(status)) { + final WordListPreference oldPreference = mCurrentPreferenceMap.get(key); + final WordListPreference pref; + if (null != oldPreference + && oldPreference.mVersion == version + && oldPreference.hasStatus(status) + && oldPreference.mLocale.equals(locale)) { + // If the old preference has all the new attributes, reuse it. Ideally, + // we should reuse the old pref even if its status is different and call + // setStatus here, but setStatus calls Preference#setSummary() which + // needs to be done on the UI thread and we're not on the UI thread + // here. We could do all this work on the UI thread, but in this case + // it's probably lighter to stay on a background thread and throw this + // old preference out. + pref = oldPreference; + } else { + // Otherwise, discard it and create a new one instead. + // TODO: when the status is different from the old one, we need to + // animate the old one out before animating the new one in. + pref = new WordListPreference(activity, mDictionaryListInterfaceState, + mClientId, wordlistId, version, locale, description, status, + filesize); + } + prefMap.put(key, pref); } - prefMap.put(key, pref); - } - } while (cursor.moveToNext()); + } while (cursor.moveToNext()); + mCurrentPreferenceMap = prefMap; + return prefMap.values(); + } + } finally { cursor.close(); - mCurrentPreferenceMap = prefMap; - return prefMap.values(); } } diff --git a/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java b/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java new file mode 100644 index 000000000..75cc7d4cb --- /dev/null +++ b/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java @@ -0,0 +1,109 @@ +/* + * 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; + +import android.app.DownloadManager; +import android.app.DownloadManager.Query; +import android.app.DownloadManager.Request; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.FileNotFoundException; + +/** + * A class to help with calling DownloadManager methods. + * + * Mostly, the problem here is that most methods from DownloadManager may throw SQL exceptions if + * they can't open the database on disk. We want to avoid crashing in these cases but can't do + * much more, so this class insulates the callers from these. SQLiteException also inherit from + * RuntimeException so they are unchecked :( + * While we're at it, we also insulate callers from the cases where DownloadManager is disabled, + * and getSystemService returns null. + */ +public class DownloadManagerWrapper { + private final static String TAG = DownloadManagerWrapper.class.getSimpleName(); + private final DownloadManager mDownloadManager; + + public DownloadManagerWrapper(final Context context) { + this((DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE)); + } + + private DownloadManagerWrapper(final DownloadManager downloadManager) { + mDownloadManager = downloadManager; + } + + public void remove(final long... ids) { + try { + if (null != mDownloadManager) { + mDownloadManager.remove(ids); + } + } catch (SQLiteException e) { + // We couldn't remove the file from DownloadManager. Apparently, the database can't + // be opened. It may be a problem with file system corruption. In any case, there is + // not much we can do apart from avoiding crashing. + Log.e(TAG, "Can't remove files with ID " + ids + " from download manager", e); + } catch (IllegalArgumentException e) { + // Not sure how this can happen, but it could be another case where the provider + // is disabled. Or it could be a bug in older versions of the framework. + Log.e(TAG, "Can't find the content URL for DownloadManager?", e); + } + } + + public ParcelFileDescriptor openDownloadedFile(final long fileId) throws FileNotFoundException { + try { + if (null != mDownloadManager) { + return mDownloadManager.openDownloadedFile(fileId); + } + } catch (SQLiteException e) { + Log.e(TAG, "Can't open downloaded file with ID " + fileId, e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Can't find the content URL for DownloadManager?", e); + } + // We come here if mDownloadManager is null or if an exception was thrown. + throw new FileNotFoundException(); + } + + public Cursor query(final Query query) { + try { + if (null != mDownloadManager) { + return mDownloadManager.query(query); + } + } catch (SQLiteException e) { + Log.e(TAG, "Can't query the download manager", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Can't find the content URL for DownloadManager?", e); + } + // We come here if mDownloadManager is null or if an exception was thrown. + return null; + } + + public long enqueue(final Request request) { + try { + if (null != mDownloadManager) { + return mDownloadManager.enqueue(request); + } + } catch (SQLiteException e) { + Log.e(TAG, "Can't enqueue a request with the download manager", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Can't find the content URL for DownloadManager?", e); + } + return 0; + } +} diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java index ff5aba6d8..4a8fa51ee 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java @@ -45,10 +45,8 @@ public class MetadataDbHelper extends SQLiteOpenHelper { // This is the first released version of the database that implements CLIENTID. It is // used to identify the versions for upgrades. This should never change going forward. private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6; - // This is the current database version. It should be updated when the database schema - // gets updated. It is passed to the framework constructor of SQLiteOpenHelper, so - // that's what the framework uses to track our database version. - private static final int METADATA_DATABASE_VERSION = 6; + // The current database version. + private static final int CURRENT_METADATA_DATABASE_VERSION = 7; private final static long NOT_A_DOWNLOAD_ID = -1; @@ -169,7 +167,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper { private MetadataDbHelper(final Context context, final String clientId) { super(context, METADATA_DATABASE_NAME_STEM + (TextUtils.isEmpty(clientId) ? "" : "." + clientId), - null, METADATA_DATABASE_VERSION); + null, CURRENT_METADATA_DATABASE_VERSION); mContext = context; mClientId = clientId; } @@ -219,22 +217,45 @@ public class MetadataDbHelper extends SQLiteOpenHelper { /** * 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. + * Version 6 and above has a DB named METADATA_DATABASE_NAME_STEM containing a + * table CLIENT_TABLE_NAME, and for each client a table called METADATA_TABLE_STEM + "." + the + * name of the client and contains a table METADATA_TABLE_NAME. + * For schemas, see the above create statements. The schemas have never changed so far. + * + * This method is called by the framework. See {@link SQLiteOpenHelper#onUpgrade} + * @param db The database we are upgrading + * @param oldVersion The old database version (the one on the disk) + * @param newVersion The new database version as supplied to the constructor of SQLiteOpenHelper */ @Override public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { if (METADATA_DATABASE_INITIAL_VERSION == oldVersion - && METADATA_DATABASE_VERSION_WITH_CLIENTID == newVersion) { + && METADATA_DATABASE_VERSION_WITH_CLIENTID <= newVersion + && CURRENT_METADATA_DATABASE_VERSION >= newVersion) { // Upgrade from version METADATA_DATABASE_INITIAL_VERSION to version // METADATA_DATABASE_VERSION_WITH_CLIENT_ID + // Only the default database should contain the client table, so we test for mClientId. if (TextUtils.isEmpty(mClientId)) { - // Only the default database should contain the client table. - // Anyway in version 3 only the default table existed so the emptyness + // Anyway in version 3 only the default table existed so the emptiness // test should always be true, but better check to be sure. createClientTable(db); } + } else if (METADATA_DATABASE_VERSION_WITH_CLIENTID < newVersion + && CURRENT_METADATA_DATABASE_VERSION >= newVersion) { + // Here we drop the client table, so that all clients send us their information again. + // The client table contains the URL to hit to update the available dictionaries list, + // but the info about the dictionaries themselves is stored in the table called + // METADATA_TABLE_NAME and we want to keep it, so we only drop the client table. + db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME); + // Only the default database should contain the client table, so we test for mClientId. + if (TextUtils.isEmpty(mClientId)) { + createClientTable(db); + } } else { - // Version 3 was the earliest version, so we should never come here. If we do, we - // have no idea what this database is, so we'd better wipe it off. + // If we're not in the above case, either we are upgrading from an earlier versionCode + // and we should wipe the database, or we are handling a version we never heard about + // (can only be a bug) so it's safer to wipe the database. db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME); db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME); onCreate(db); @@ -533,12 +554,17 @@ public class MetadataDbHelper extends SQLiteOpenHelper { PENDINGID_COLUMN + "= ?", new String[] { Long.toString(id) }, null, null, null); - // There should never be more than one result. If because of some bug there are, returning - // only one result is the right thing to do, because we couldn't handle several anyway - // and we should still handle one. - final ContentValues result = getFirstLineAsContentValues(cursor); - cursor.close(); - return result; + if (null == cursor) { + return null; + } + try { + // There should never be more than one result. If because of some bug there are, + // returning only one result is the right thing to do, because we couldn't handle + // several anyway and we should still handle one. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } } /** @@ -559,11 +585,16 @@ public class MetadataDbHelper extends SQLiteOpenHelper { new String[] { id, Integer.toString(STATUS_INSTALLED), Integer.toString(STATUS_DELETING) }, null, null, null); - // There should only be one result, but if there are several, we can't tell which - // is the best, so we just return the first one. - final ContentValues result = getFirstLineAsContentValues(cursor); - cursor.close(); - return result; + if (null == cursor) { + return null; + } + try { + // There should only be one result, but if there are several, we can't tell which + // is the best, so we just return the first one. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } } /** @@ -622,10 +653,15 @@ public class MetadataDbHelper extends SQLiteOpenHelper { METADATA_TABLE_COLUMNS, WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ?", new String[] { id, Integer.toString(version) }, null, null, null); - // This is a lookup by primary key, so there can't be more than one result. - final ContentValues result = getFirstLineAsContentValues(cursor); - cursor.close(); - return result; + if (null == cursor) { + return null; + } + try { + // This is a lookup by primary key, so there can't be more than one result. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } } /** @@ -641,10 +677,15 @@ public class MetadataDbHelper extends SQLiteOpenHelper { METADATA_TABLE_COLUMNS, WORDLISTID_COLUMN + "= ?", new String[] { id }, null, null, VERSION_COLUMN + " DESC", "1"); - // This is a lookup by primary key, so there can't be more than one result. - final ContentValues result = getFirstLineAsContentValues(cursor); - cursor.close(); - return result; + if (null == cursor) { + return null; + } + try { + // This is a lookup by primary key, so there can't be more than one result. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } } /** diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java index a0147b6d6..5c2289911 100644 --- a/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/MetadataHandler.java @@ -44,8 +44,7 @@ public class MetadataHandler { */ private static List<WordListMetadata> makeMetadataObject(final Cursor results) { final ArrayList<WordListMetadata> buildingMetadata = new ArrayList<WordListMetadata>(); - - if (results.moveToFirst()) { + if (null != results && results.moveToFirst()) { final int localeColumn = results.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN); final int typeColumn = results.getColumnIndex(MetadataDbHelper.TYPE_COLUMN); final int descriptionColumn = @@ -61,7 +60,6 @@ public class MetadataHandler { final int versionIndex = results.getColumnIndex(MetadataDbHelper.VERSION_COLUMN); final int formatVersionIndex = results.getColumnIndex(MetadataDbHelper.FORMATVERSION_COLUMN); - do { buildingMetadata.add(new WordListMetadata(results.getString(idIndex), results.getInt(typeColumn), @@ -75,8 +73,6 @@ public class MetadataHandler { results.getInt(formatVersionIndex), 0, results.getString(localeColumn))); } while (results.moveToNext()); - - results.close(); } return Collections.unmodifiableList(buildingMetadata); } @@ -92,9 +88,14 @@ public class MetadataHandler { // If clientId is null, we get a cursor on the default database (see // MetadataDbHelper#getInstance() for more on this) final Cursor results = MetadataDbHelper.queryCurrentMetadata(context, clientId); - final List<WordListMetadata> resultList = makeMetadataObject(results); - results.close(); - return resultList; + // If null, we should return makeMetadataObject(null), so we go through. + try { + return makeMetadataObject(results); + } finally { + if (null != results) { + results.close(); + } + } } /** diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java index 0e7c3bb7e..dcff490db 100644 --- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java +++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java @@ -249,13 +249,7 @@ public final class UpdateHandler { metadataRequest.setVisibleInDownloadsUi( res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI)); - final DownloadManager manager = - (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); - if (null == manager) { - // Download manager is not installed or disabled. - // TODO: fall back to self-managed download? - return; - } + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); cancelUpdateWithDownloadManager(context, metadataUri, manager); final long downloadId; synchronized (sSharedIdProtector) { @@ -278,10 +272,10 @@ public final class UpdateHandler { * * @param context the context to open the database on * @param metadataUri the URI to cancel - * @param manager an instance of DownloadManager + * @param manager an wrapped instance of DownloadManager */ private static void cancelUpdateWithDownloadManager(final Context context, - final String metadataUri, final DownloadManager manager) { + final String metadataUri, final DownloadManagerWrapper manager) { synchronized (sSharedIdProtector) { final long metadataDownloadId = MetadataDbHelper.getMetadataDownloadIdForURI(context, metadataUri); @@ -306,10 +300,9 @@ public final class UpdateHandler { * @param clientId the ID of the client we want to cancel the update of */ public static void cancelUpdate(final Context context, final String clientId) { - final DownloadManager manager = - (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId); - if (null != manager) cancelUpdateWithDownloadManager(context, metadataUri, manager); + cancelUpdateWithDownloadManager(context, metadataUri, manager); } /** @@ -323,15 +316,15 @@ public final class UpdateHandler { * download request id, which is not known before submitting the request to the download * manager. Hence, it only updates the relevant line. * - * @param manager the download manager service to register the request with. + * @param manager a wrapped download manager service to register the request with. * @param request the request to register. * @param db the metadata database. * @param id the id of the word list. * @param version the version of the word list. * @return the download id returned by the download manager. */ - public static long registerDownloadRequest(final DownloadManager manager, final Request request, - final SQLiteDatabase db, final String id, final int version) { + public static long registerDownloadRequest(final DownloadManagerWrapper manager, + final Request request, final SQLiteDatabase db, final String id, final int version) { DebugLogUtils.l("RegisterDownloadRequest for word list id : ", id, ", version ", version); final long downloadId; synchronized (sSharedIdProtector) { @@ -345,8 +338,8 @@ public final class UpdateHandler { /** * Retrieve information about a specific download from DownloadManager. */ - private static CompletedDownloadInfo getCompletedDownloadInfo(final DownloadManager manager, - final long downloadId) { + private static CompletedDownloadInfo getCompletedDownloadInfo( + final DownloadManagerWrapper manager, final long downloadId) { final Query query = new Query().setFilterById(downloadId); final Cursor cursor = manager.query(query); @@ -425,8 +418,7 @@ public final class UpdateHandler { DebugLogUtils.l("DownloadFinished with id", fileId); if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore - final DownloadManager manager = - (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); final CompletedDownloadInfo downloadInfo = getCompletedDownloadInfo(manager, fileId); final ArrayList<DownloadRecord> recordList = @@ -517,7 +509,7 @@ public final class UpdateHandler { } private static boolean handleDownloadedFile(final Context context, - final DownloadRecord downloadRecord, final DownloadManager manager, + final DownloadRecord downloadRecord, final DownloadManagerWrapper manager, final long fileId) { try { // {@link handleWordList(Context,InputStream,ContentValues)}. diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java index ba1fce1a8..aea16af0d 100644 --- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java +++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java @@ -98,6 +98,10 @@ public final class WordListPreference extends Preference { setSummary(getSummary(status)); } + public boolean hasStatus(final int status) { + return status == mStatus; + } + @Override public View onCreateView(final ViewGroup parent) { final View orphanedView = mInterfaceState.findFirstOrphanedView(); @@ -217,6 +221,7 @@ public final class WordListPreference extends Preference { progressBar.setIds(mClientId, mWordlistId); progressBar.setMax(mFilesize); final boolean showProgressBar = (MetadataDbHelper.STATUS_DOWNLOADING == mStatus); + setSummary(getSummary(mStatus)); status.setVisibility(showProgressBar ? View.INVISIBLE : View.VISIBLE); progressBar.setVisibility(showProgressBar ? View.VISIBLE : View.INVISIBLE); diff --git a/java/src/com/android/inputmethod/event/Combiner.java b/java/src/com/android/inputmethod/event/Combiner.java index ab6b70c04..8b808c6b3 100644 --- a/java/src/com/android/inputmethod/event/Combiner.java +++ b/java/src/com/android/inputmethod/event/Combiner.java @@ -16,14 +16,33 @@ package com.android.inputmethod.event; +import java.util.ArrayList; + /** - * A generic interface for combiners. + * A generic interface for combiners. Combiners are objects that transform chains of input events + * into committable strings and manage feedback to show to the user on the combining state. */ public interface Combiner { /** - * Combine an event with the existing state and return the new event. + * Process an event, possibly combining it with the existing state and return the new event. + * + * If this event does not result in any new event getting passed down the chain, this method + * returns null. It may also modify the previous event list if appropriate. + * + * @param previousEvents the previous events in this composition. * @param event the event to combine with the existing state. * @return the resulting event. */ - Event combine(Event event); + Event processEvent(ArrayList<Event> previousEvents, Event event); + + /** + * Get the feedback that should be shown to the user for the current state of this combiner. + * @return A CharSequence representing the feedback to show users. It may include styles. + */ + CharSequence getCombiningStateFeedback(); + + /** + * Reset the state of this combiner, for example when the cursor was moved. + */ + void reset(); } diff --git a/java/src/com/android/inputmethod/event/CombinerChain.java b/java/src/com/android/inputmethod/event/CombinerChain.java new file mode 100644 index 000000000..8b59dc52a --- /dev/null +++ b/java/src/com/android/inputmethod/event/CombinerChain.java @@ -0,0 +1,117 @@ +/* + * 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.event; + +import android.text.SpannableStringBuilder; +import android.text.TextUtils; + +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.utils.CollectionUtils; + +import java.util.ArrayList; + +/** + * This class implements the logic chain between receiving events and generating code points. + * + * Event sources are multiple. It may be a hardware keyboard, a D-PAD, a software keyboard, + * or any exotic input source. + * This class will orchestrate the composing chain that starts with an event as its input. Each + * composer will be given turns one after the other. + * The output is composed of two sequences of code points: the first, representing the already + * finished combining part, will be shown normally as the composing string, while the second is + * feedback on the composing state and will typically be shown with different styling such as + * a colored background. + */ +public class CombinerChain { + // The already combined text, as described above + private StringBuilder mCombinedText; + // The feedback on the composing state, as described above + private SpannableStringBuilder mStateFeedback; + private final ArrayList<Combiner> mCombiners; + + /** + * Create an combiner chain. + * + * The combiner chain takes events as inputs and outputs code points and combining state. + * For example, if the input language is Japanese, the combining chain will typically perform + * kana conversion. + * + * @param combinerList A list of combiners to be applied in order. + */ + public CombinerChain(final Combiner... combinerList) { + mCombiners = CollectionUtils.newArrayList(); + // The dead key combiner is always active, and always first + mCombiners.add(new DeadKeyCombiner()); + mCombinedText = new StringBuilder(); + mStateFeedback = new SpannableStringBuilder(); + } + + public void reset() { + mCombinedText.setLength(0); + mStateFeedback.clear(); + for (final Combiner c : mCombiners) { + c.reset(); + } + } + + /** + * Pass a new event through the whole chain. + * @param previousEvents the list of previous events in this composition + * @param newEvent the new event to process + */ + public void processEvent(final ArrayList<Event> previousEvents, final Event newEvent) { + final ArrayList<Event> modifiablePreviousEvents = new ArrayList<Event>(previousEvents); + Event event = newEvent; + for (final Combiner combiner : mCombiners) { + // A combiner can never return more than one event; it can return several + // code points, but they should be encapsulated within one event. + event = combiner.processEvent(modifiablePreviousEvents, event); + if (null == event) { + // Combiners return null if they eat the event. + break; + } + } + if (null != event) { + // TODO: figure out the generic way of doing this + if (Constants.CODE_DELETE == event.mKeyCode) { + final int length = mCombinedText.length(); + if (length > 0) { + final int lastCodePoint = mCombinedText.codePointBefore(length); + mCombinedText.delete(length - Character.charCount(lastCodePoint), length); + } + } else { + final CharSequence textToCommit = event.getTextToCommit(); + if (!TextUtils.isEmpty(textToCommit)) { + mCombinedText.append(textToCommit); + } + } + } + mStateFeedback.clear(); + for (int i = mCombiners.size() - 1; i >= 0; --i) { + mStateFeedback.append(mCombiners.get(i).getCombiningStateFeedback()); + } + } + + /** + * Get the char sequence that should be displayed as the composing word. It may include + * styling spans. + */ + public CharSequence getComposingWordWithCombiningFeedback() { + final SpannableStringBuilder s = new SpannableStringBuilder(mCombinedText); + return s.append(mStateFeedback); + } +} diff --git a/java/src/com/android/inputmethod/event/DeadKeyCombiner.java b/java/src/com/android/inputmethod/event/DeadKeyCombiner.java index 52987d571..bef4d8594 100644 --- a/java/src/com/android/inputmethod/event/DeadKeyCombiner.java +++ b/java/src/com/android/inputmethod/event/DeadKeyCombiner.java @@ -21,14 +21,17 @@ import android.view.KeyCharacterMap; import com.android.inputmethod.latin.Constants; +import java.util.ArrayList; + /** * A combiner that handles dead keys. */ public class DeadKeyCombiner implements Combiner { + // TODO: make this a list of events instead final StringBuilder mDeadSequence = new StringBuilder(); @Override - public Event combine(final Event event) { + public Event processEvent(final ArrayList<Event> previousEvents, final Event event) { if (null == event) return null; // Just in case some combiner is broken if (TextUtils.isEmpty(mDeadSequence)) { if (event.isDead()) { @@ -43,19 +46,33 @@ public class DeadKeyCombiner implements Combiner { 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 committable + // 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. - return Event.createCommittableEvent(deadCodePoint, - Constants.CODE_SPACE == event.mCodePoint ? null : event /* next */); + // 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 */); } else { // We could combine the characters. - return Event.createCommittableEvent(resultingCodePoint, null /* next */); + return Event.createHardwareKeypressEvent(resultingCodePoint, event.mKeyCode, + null /* next */, false /* isKeyRepeat */); } } } + @Override + public void reset() { + mDeadSequence.setLength(0); + } + + @Override + public CharSequence getCombiningStateFeedback() { + return mDeadSequence; + } } diff --git a/java/src/com/android/inputmethod/event/Event.java b/java/src/com/android/inputmethod/event/Event.java index 1f3320eb7..4a9163c8e 100644 --- a/java/src/com/android/inputmethod/event/Event.java +++ b/java/src/com/android/inputmethod/event/Event.java @@ -16,6 +16,10 @@ package com.android.inputmethod.event; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.utils.StringUtils; + /** * Class representing a generic input event as handled by Latin IME. * @@ -32,62 +36,227 @@ public class Event { // Should the types below be represented by separate classes instead? It would be cleaner // but probably a bit too much // An event we don't handle in Latin IME, for example pressing Ctrl on a hardware keyboard. - final public static int EVENT_NOT_HANDLED = 0; - // A character that is already final, for example pressing an alphabetic character on a - // hardware qwerty keyboard. - final public static int EVENT_COMMITTABLE = 1; - // A dead key, which means a character that should combine with what is coming next. Examples - // include the "^" character on an azerty keyboard which combines with "e" to make "ê", or - // AltGr+' on a dvorak international keyboard which combines with "e" to make "é". This is - // true regardless of the language or combining mode, and should be seen as a property of the - // key - a dead key followed by another key with which it can combine should be regarded as if - // the keyboard actually had such a key. - final public static int EVENT_DEAD = 2; + final public static int EVENT_TYPE_NOT_HANDLED = 0; + // A key press that is part of input, for example pressing an alphabetic character on a + // hardware qwerty keyboard. It may be part of a sequence that will be re-interpreted later + // through combination. + final public static int EVENT_TYPE_INPUT_KEYPRESS = 1; // A toggle event is triggered by a key that affects the previous character. An example would // be a numeric key on a 10-key keyboard, which would toggle between 1 - a - b - c with // repeated presses. - final public static int EVENT_TOGGLE = 3; + final public static int EVENT_TYPE_TOGGLE = 2; // A mode event instructs the combiner to change modes. The canonical example would be the // hankaku/zenkaku key on a Japanese keyboard, or even the caps lock key on a qwerty keyboard // if handled at the combiner level. - final public static int EVENT_MODE_KEY = 4; + final public static int EVENT_TYPE_MODE_KEY = 3; + // An event corresponding to a gesture. + final public static int EVENT_TYPE_GESTURE = 4; + // An event corresponding to the manual pick of a suggestion. + final public static int EVENT_TYPE_SUGGESTION_PICKED = 5; + // An event corresponding to a string generated by some software process. + final public static int EVENT_TYPE_SOFTWARE_GENERATED_STRING = 6; + + // 0 is a valid code point, so we use -1 here. + final public static int NOT_A_CODE_POINT = -1; + // -1 is a valid key code, so we use 0 here. + final public static int NOT_A_KEY_CODE = 0; - final private static int NOT_A_CODE_POINT = 0; + final private static int FLAG_NONE = 0; + // This event is a dead character, usually input by a dead key. Examples include dead-acute + // or dead-abovering. + final private static int FLAG_DEAD = 0x1; + // This event is coming from a key repeat, software or hardware. + final private static int FLAG_REPEAT = 0x2; - final private int mType; // The type of event - one of the constants above + final private int mEventType; // The type of event - one of the constants above // The code point associated with the event, if relevant. This is a unicode code point, and // has nothing to do with other representations of the key. It is only relevant if this event - // is the right type: COMMITTABLE or DEAD or TOGGLE, but for a mode key like hankaku/zenkaku or - // ctrl, there is no code point associated so this should be NOT_A_CODE_POINT to avoid - // unintentional use of its value when it's not relevant. + // is of KEYPRESS type, but for a mode key like hankaku/zenkaku or ctrl, there is no code point + // associated so this should be NOT_A_CODE_POINT to avoid unintentional use of its value when + // it's not relevant. final public int mCodePoint; + + // If applicable, this contains the string that should be input. + final public CharSequence mText; + + // The key code associated with the event, if relevant. This is relevant whenever this event + // has been triggered by a key press, but not for a gesture for example. This has conceptually + // no link to the code point, although keys that enter a straight code point may often set + // this to be equal to mCodePoint for convenience. If this is not a key, this must contain + // NOT_A_KEY_CODE. + final public int mKeyCode; + + // Coordinates of the touch event, if relevant. If useful, we may want to replace this with + // a MotionEvent or something in the future. This is only relevant when the keypress is from + // a software keyboard obviously, unless there are touch-sensitive hardware keyboards in the + // future or some other awesome sauce. + final public int mX; + final public int mY; + + // Some flags that can't go into the key code. It's a bit field of FLAG_* + final private int mFlags; + + // If this is of type EVENT_TYPE_SUGGESTION_PICKED, this must not be null (and must be null in + // other cases). + final public SuggestedWordInfo mSuggestedWordInfo; + // The next event, if any. Null if there is no next event yet. final public Event mNextEvent; // This method is private - to create a new event, use one of the create* utility methods. - private Event(final int type, final int codePoint, final Event next) { - mType = type; + private Event(final int type, final CharSequence text, final int codePoint, final int keyCode, + final int x, final int y, final SuggestedWordInfo suggestedWordInfo, final int flags, + final Event next) { + mEventType = type; + mText = text; mCodePoint = codePoint; + mKeyCode = keyCode; + mX = x; + mY = y; + mSuggestedWordInfo = suggestedWordInfo; + mFlags = flags; mNextEvent = next; + // Sanity checks + // mSuggestedWordInfo is non-null if and only if the type is SUGGESTION_PICKED + if (EVENT_TYPE_SUGGESTION_PICKED == mEventType) { + if (null == mSuggestedWordInfo) { + throw new RuntimeException("Wrong event: SUGGESTION_PICKED event must have a " + + "non-null SuggestedWordInfo"); + } + } else { + if (null != mSuggestedWordInfo) { + throw new RuntimeException("Wrong event: only SUGGESTION_PICKED events may have " + + "a non-null SuggestedWordInfo"); + } + } } - public static Event createDeadEvent(final int codePoint, final Event next) { - return new Event(EVENT_DEAD, codePoint, next); + public static Event createSoftwareKeypressEvent(final int codePoint, final int keyCode, + final int x, final int y, final boolean isKeyRepeat) { + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, x, y, + null /* suggestedWordInfo */, isKeyRepeat ? FLAG_REPEAT : FLAG_NONE, null); } - public static Event createCommittableEvent(final int codePoint, final Event next) { - return new Event(EVENT_COMMITTABLE, codePoint, next); + public static Event createHardwareKeypressEvent(final int codePoint, final int keyCode, + final Event next, final boolean isKeyRepeat) { + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, + Constants.EXTERNAL_KEYBOARD_COORDINATE, Constants.EXTERNAL_KEYBOARD_COORDINATE, + null /* suggestedWordInfo */, isKeyRepeat ? FLAG_REPEAT : FLAG_NONE, next); } - public static Event createNotHandledEvent() { - return new Event(EVENT_NOT_HANDLED, NOT_A_CODE_POINT, null); + // This creates an input event for a dead character. @see {@link #FLAG_DEAD} + 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, + Constants.EXTERNAL_KEYBOARD_COORDINATE, Constants.EXTERNAL_KEYBOARD_COORDINATE, + null /* suggestedWordInfo */, FLAG_DEAD, next); + } + + /** + * Create an input event with nothing but a code point. This is the most basic possible input + * event; it contains no information on many things the IME requires to function correctly, + * so avoid using it unless really nothing is known about this input. + * @param codePoint the code point. + * @return an event for this code point. + */ + public static Event createEventForCodePointFromUnknownSource(final int codePoint) { + // TODO: should we have a different type of event for this? After all, it's not a key press. + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event with a code point and x, y coordinates. This is typically used when + * resuming a previously-typed word, when the coordinates are still known. + * @param codePoint the code point to input. + * @param x the X coordinate. + * @param y the Y coordinate. + * @return an event for this code point and coordinates. + */ + public static Event createEventForCodePointFromAlreadyTypedText(final int codePoint, + final int x, final int y) { + // TODO: should we have a different type of event for this? After all, it's not a key press. + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, NOT_A_KEY_CODE, + x, y, null /* suggestedWordInfo */, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event representing the manual pick of a suggestion. + * @return an event for this suggestion pick. + */ + public static Event createSuggestionPickedEvent(final SuggestedWordInfo suggestedWordInfo) { + return new Event(EVENT_TYPE_SUGGESTION_PICKED, suggestedWordInfo.mWord, + NOT_A_CODE_POINT, NOT_A_KEY_CODE, + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, + suggestedWordInfo, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event with a CharSequence. This is used by some software processes whose + * output is a string, possibly with styling. Examples include press on a multi-character key, + * or combination that outputs a string. + * @param text the CharSequence associated with this event. + * @param keyCode the key code, or NOT_A_KEYCODE if not applicable. + * @return an event for this text. + */ + public static Event createSoftwareTextEvent(final CharSequence text, final int keyCode) { + return new Event(EVENT_TYPE_SOFTWARE_GENERATED_STRING, text, NOT_A_CODE_POINT, keyCode, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null /* next */); } - public boolean isCommittable() { - return EVENT_COMMITTABLE == mType; + /** + * Creates an input event representing the manual pick of a punctuation suggestion. + * @return an event for this suggestion pick. + */ + public static Event createPunctuationSuggestionPickedEvent( + final SuggestedWordInfo suggestedWordInfo) { + final int primaryCode = suggestedWordInfo.mWord.charAt(0); + return new Event(EVENT_TYPE_SUGGESTION_PICKED, suggestedWordInfo.mWord, primaryCode, + NOT_A_KEY_CODE, Constants.SUGGESTION_STRIP_COORDINATE, + Constants.SUGGESTION_STRIP_COORDINATE, suggestedWordInfo, FLAG_NONE, + null /* next */); } + public static Event createNotHandledEvent() { + return new Event(EVENT_TYPE_NOT_HANDLED, null /* text */, NOT_A_CODE_POINT, NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null); + } + + // Returns whether this event is for a dead character. @see {@link #FLAG_DEAD} public boolean isDead() { - return EVENT_DEAD == mType; + return 0 != (FLAG_DEAD & mFlags); + } + + public boolean isKeyRepeat() { + return 0 != (FLAG_REPEAT & mFlags); + } + + // Returns whether this is a fake key press from the suggestion strip. This happens with + // punctuation signs selected from the suggestion strip. + public boolean isSuggestionStripPress() { + return EVENT_TYPE_SUGGESTION_PICKED == mEventType; + } + + public boolean isHandled() { + return EVENT_TYPE_NOT_HANDLED != mEventType; + } + + public CharSequence getTextToCommit() { + switch (mEventType) { + case EVENT_TYPE_MODE_KEY: + case EVENT_TYPE_NOT_HANDLED: + case EVENT_TYPE_TOGGLE: + return ""; + case EVENT_TYPE_INPUT_KEYPRESS: + return StringUtils.newSingleCodePointString(mCodePoint); + case EVENT_TYPE_GESTURE: + case EVENT_TYPE_SOFTWARE_GENERATED_STRING: + case EVENT_TYPE_SUGGESTION_PICKED: + return mText; + } + throw new RuntimeException("Unknown event type: " + mEventType); } } diff --git a/java/src/com/android/inputmethod/event/EventInterpreter.java b/java/src/com/android/inputmethod/event/EventInterpreter.java deleted file mode 100644 index 726b9206b..000000000 --- a/java/src/com/android/inputmethod/event/EventInterpreter.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2012 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.event; - -import android.util.SparseArray; -import android.view.KeyEvent; - -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.LatinIME; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.util.ArrayList; - -/** - * This class implements the logic between receiving events and generating code points. - * - * Event sources are multiple. It may be a hardware keyboard, a D-PAD, a software keyboard, - * or any exotic input source. - * This class will orchestrate the decoding chain that starts with an event and ends up with - * a stream of code points + decoding state. - */ -public class EventInterpreter { - // TODO: Implement an object pool for events, as we'll create a lot of them - // TODO: Create a combiner - // TODO: Create an object type to represent input material + visual feedback + decoding state - // TODO: Create an interface to call back to Latin IME through the above object - - final EventDecoderSpec mDecoderSpec; - final SparseArray<HardwareEventDecoder> mHardwareEventDecoders; - final SoftwareEventDecoder mSoftwareEventDecoder; - final LatinIME mLatinIme; - final ArrayList<Combiner> mCombiners; - - /** - * Create a default interpreter. - * - * This creates a default interpreter that does nothing. A default interpreter should normally - * only be used for fallback purposes, when we really don't know what we want to do with input. - * - * @param latinIme a reference to the ime. - */ - public EventInterpreter(final LatinIME latinIme) { - this(null, latinIme); - } - - /** - * Create an event interpreter according to a specification. - * - * The specification contains information about what to do with events. Typically, it will - * contain information about the type of keyboards - for example, if hardware keyboard(s) is/are - * attached, their type will be included here so that the decoder knows what to do with each - * keypress (a 10-key keyboard is not handled like a qwerty-ish keyboard). - * It also contains information for combining characters. For example, if the input language - * is Japanese, the specification will typically request kana conversion. - * Also note that the specification can be null. This means that we need to create a default - * interpreter that does no specific combining, and assumes the most common cases. - * - * @param specification the specification for event interpretation. null for default. - * @param latinIme a reference to the ime. - */ - public EventInterpreter(final EventDecoderSpec specification, final LatinIME latinIme) { - mDecoderSpec = null != specification ? specification : new EventDecoderSpec(); - // For both, we expect to have only one decoder in almost all cases, hence the default - // capacity of 1. - mHardwareEventDecoders = new SparseArray<HardwareEventDecoder>(1); - mSoftwareEventDecoder = new SoftwareKeyboardEventDecoder(); - mCombiners = CollectionUtils.newArrayList(); - mCombiners.add(new DeadKeyCombiner()); - mLatinIme = latinIme; - } - - // Helper method to decode a hardware key event into a generic event, and execute any - // necessary action. - public boolean onHardwareKeyEvent(final KeyEvent hardwareKeyEvent) { - final Event decodedEvent = getHardwareKeyEventDecoder(hardwareKeyEvent.getDeviceId()) - .decodeHardwareKey(hardwareKeyEvent); - return onEvent(decodedEvent); - } - - public boolean onSoftwareEvent() { - final Event decodedEvent = getSoftwareEventDecoder().decodeSoftwareEvent(); - return onEvent(decodedEvent); - } - - private HardwareEventDecoder getHardwareKeyEventDecoder(final int deviceId) { - final HardwareEventDecoder decoder = mHardwareEventDecoders.get(deviceId); - if (null != decoder) return decoder; - // TODO: create the decoder according to the specification - final HardwareEventDecoder newDecoder = new HardwareKeyboardEventDecoder(deviceId); - mHardwareEventDecoders.put(deviceId, newDecoder); - return newDecoder; - } - - private SoftwareEventDecoder getSoftwareEventDecoder() { - // Within the context of Latin IME, since we never present several software interfaces - // at the time, we should never need multiple software event decoders at a time. - return mSoftwareEventDecoder; - } - - private boolean onEvent(final Event event) { - Event currentlyProcessingEvent = event; - boolean processed = false; - for (int i = 0; i < mCombiners.size(); ++i) { - currentlyProcessingEvent = mCombiners.get(i).combine(event); - } - while (null != currentlyProcessingEvent) { - if (currentlyProcessingEvent.isCommittable()) { - mLatinIme.onCodeInput(currentlyProcessingEvent.mCodePoint, - Constants.EXTERNAL_KEYBOARD_COORDINATE, - Constants.EXTERNAL_KEYBOARD_COORDINATE); - processed = true; - } else if (event.isDead()) { - processed = true; - } - currentlyProcessingEvent = currentlyProcessingEvent.mNextEvent; - } - return processed; - } -} diff --git a/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java b/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java index 720d07433..05ba99923 100644 --- a/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java +++ b/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java @@ -46,28 +46,36 @@ public class HardwareKeyboardEventDecoder implements HardwareEventDecoder { // do not necessarily map to a unicode character. This represents a physical key, like // the key for 'A' or Space, but also Backspace or Ctrl or Caps Lock. final int keyCode = keyEvent.getKeyCode(); + final boolean isKeyRepeat = (0 != keyEvent.getRepeatCount()); if (KeyEvent.KEYCODE_DEL == keyCode) { - return Event.createCommittableEvent(Constants.CODE_DELETE, null /* next */); + return Event.createHardwareKeypressEvent(Event.NOT_A_CODE_POINT, Constants.CODE_DELETE, + null /* next */, isKeyRepeat); } if (keyEvent.isPrintingKey() || KeyEvent.KEYCODE_SPACE == keyCode || KeyEvent.KEYCODE_ENTER == keyCode) { if (0 != (codePointAndFlags & KeyCharacterMap.COMBINING_ACCENT)) { // A dead key. return Event.createDeadEvent( - codePointAndFlags & KeyCharacterMap.COMBINING_ACCENT_MASK, null /* next */); + codePointAndFlags & KeyCharacterMap.COMBINING_ACCENT_MASK, keyCode, + null /* next */); } if (KeyEvent.KEYCODE_ENTER == keyCode) { // The Enter key. If the Shift key is not being pressed, this should send a // CODE_ENTER to trigger the action if any, or a carriage return otherwise. If the // Shift key is being pressed, this should send a CODE_SHIFT_ENTER and let // Latin IME decide what to do with it. - return Event.createCommittableEvent(keyEvent.isShiftPressed() - ? Constants.CODE_SHIFT_ENTER : Constants.CODE_ENTER, - null /* next */); + if (keyEvent.isShiftPressed()) { + return Event.createHardwareKeypressEvent(Event.NOT_A_CODE_POINT, + Constants.CODE_SHIFT_ENTER, null /* next */, isKeyRepeat); + } else { + return Event.createHardwareKeypressEvent(Constants.CODE_ENTER, keyCode, + null /* next */, isKeyRepeat); + } } - // If not Enter, then we have a committable character. This should be committed - // right away, taking into account the current state. - return Event.createCommittableEvent(codePointAndFlags, null /* next */); + // If not Enter, then this is just a regular keypress event for a normal character + // that can be committed right away, taking into account the current state. + return Event.createHardwareKeypressEvent(keyCode, codePointAndFlags, null /* next */, + isKeyRepeat); } return Event.createNotHandledEvent(); } diff --git a/java/src/com/android/inputmethod/event/InputTransaction.java b/java/src/com/android/inputmethod/event/InputTransaction.java new file mode 100644 index 000000000..4fe9b403e --- /dev/null +++ b/java/src/com/android/inputmethod/event/InputTransaction.java @@ -0,0 +1,84 @@ +/* + * 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.event; + +import com.android.inputmethod.latin.settings.SettingsValues; + +/** + * An object encapsulating a single transaction for input. + */ +public class InputTransaction { + // UPDATE_LATER is stronger than UPDATE_NOW. The reason for this is, if we have to update later, + // it's because something will change that we can't evaluate now, which means that even if we + // re-evaluate now we'll have to do it again later. The only case where that wouldn't apply + // would be if we needed to update now to find out the new state right away, but then we + // can't do it with this deferred mechanism anyway. + public static final int SHIFT_NO_UPDATE = 0; + public static final int SHIFT_UPDATE_NOW = 1; + public static final int SHIFT_UPDATE_LATER = 2; + + // Initial conditions + public final SettingsValues mSettingsValues; + public final Event mEvent; + public final long mTimestamp; + public final int mSpaceState; + public final int mShiftState; + + // Outputs + private int mRequiredShiftUpdate = SHIFT_NO_UPDATE; + private boolean mRequiresUpdateSuggestions = false; + + public InputTransaction(final SettingsValues settingsValues, final Event event, + final long timestamp, final int spaceState, final int shiftState) { + mSettingsValues = settingsValues; + mEvent = event; + mTimestamp = timestamp; + mSpaceState = spaceState; + mShiftState = shiftState; + } + + /** + * Indicate that this transaction requires some type of shift update. + * @param updateType What type of shift update this requires. + */ + public void requireShiftUpdate(final int updateType) { + mRequiredShiftUpdate = Math.max(mRequiredShiftUpdate, updateType); + } + + /** + * Gets what type of shift update this transaction requires. + * @return The shift update type. + */ + public int getRequiredShiftUpdate() { + return mRequiredShiftUpdate; + } + + /** + * Indicate that this transaction requires updating the suggestions. + */ + public void setRequiresUpdateSuggestions() { + mRequiresUpdateSuggestions = true; + } + + /** + * Find out whether this transaction requires updating the suggestions. + * @return Whether this transaction requires updating the suggestions. + */ + public boolean requiresUpdateSuggestions() { + return mRequiresUpdateSuggestions; + } +} diff --git a/java/src/com/android/inputmethod/event/SoftwareEventDecoder.java b/java/src/com/android/inputmethod/event/SoftwareEventDecoder.java deleted file mode 100644 index d81ee0b37..000000000 --- a/java/src/com/android/inputmethod/event/SoftwareEventDecoder.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2012 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.event; - -/** - * An event decoder for events out of a software keyboard. - * - * This defines the interface for an event decoder that supports events out of a software keyboard. - * This differs significantly from hardware keyboard event decoders in several respects. First, - * a software keyboard does not have a scancode/layout system; the keypresses that insert - * characters output unicode characters directly. - */ -public interface SoftwareEventDecoder extends EventDecoder { - public Event decodeSoftwareEvent(); -} diff --git a/java/src/com/android/inputmethod/event/SoftwareKeyboardEventDecoder.java b/java/src/com/android/inputmethod/event/SoftwareKeyboardEventDecoder.java deleted file mode 100644 index de91567c7..000000000 --- a/java/src/com/android/inputmethod/event/SoftwareKeyboardEventDecoder.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2012 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.event; - -/** - * A decoder for events from software keyboard, like the ones displayed by Latin IME. - */ -public class SoftwareKeyboardEventDecoder implements SoftwareEventDecoder { - @Override - public Event decodeSoftwareEvent() { - return null; - } -} diff --git a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java b/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java index e23131a30..d56a3cf25 100644 --- a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java +++ b/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java @@ -31,17 +31,17 @@ public class EmojiCategoryPageIndicatorView extends LinearLayout { private int mCurrentCategoryPageId = 0; private float mOffset = 0.0f; - public EmojiCategoryPageIndicatorView(Context context) { + public EmojiCategoryPageIndicatorView(final Context context) { this(context, null /* attrs */); } - public EmojiCategoryPageIndicatorView(Context context, AttributeSet attrs) { + public EmojiCategoryPageIndicatorView(final Context context, final AttributeSet attrs) { super(context, attrs); mPaint.setColor(context.getResources().getColor( R.color.emoji_category_page_id_view_foreground)); } - public void setCategoryPageId(int size, int id, float offset) { + public void setCategoryPageId(final int size, final int id, final float offset) { mCategoryPageSize = size; mCurrentCategoryPageId = id; mOffset = offset; @@ -49,7 +49,7 @@ public class EmojiCategoryPageIndicatorView extends LinearLayout { } @Override - protected void onDraw(Canvas canvas) { + protected void onDraw(final Canvas canvas) { if (mCategoryPageSize <= 1) { // If the category is not set yet or contains only one category, // just clear and return. diff --git a/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java b/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java index f12373503..ad996971f 100644 --- a/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java +++ b/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java @@ -23,16 +23,18 @@ import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Color; import android.graphics.Rect; import android.os.Build; +import android.os.CountDownTimer; import android.preference.PreferenceManager; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; -import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; import android.util.Pair; import android.util.SparseArray; +import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -44,8 +46,10 @@ import android.widget.TabHost.OnTabChangeListener; import android.widget.TextView; import com.android.inputmethod.keyboard.internal.DynamicGridKeyboard; -import com.android.inputmethod.keyboard.internal.ScrollKeyboardView; -import com.android.inputmethod.keyboard.internal.ScrollViewWithNotifier; +import com.android.inputmethod.keyboard.internal.EmojiLayoutParams; +import com.android.inputmethod.keyboard.internal.EmojiPageKeyboardView; +import com.android.inputmethod.keyboard.internal.KeyDrawParams; +import com.android.inputmethod.keyboard.internal.KeyVisualAttributes; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SubtypeSwitcher; @@ -58,6 +62,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; /** * View class to implement Emoji palettes. @@ -71,17 +76,19 @@ import java.util.concurrent.ConcurrentHashMap; * Because of the above reasons, this class doesn't extend {@link KeyboardView}. */ public final class EmojiPalettesView extends LinearLayout implements OnTabChangeListener, - ViewPager.OnPageChangeListener, View.OnClickListener, - ScrollKeyboardView.OnKeyClickListener { - private static final String TAG = EmojiPalettesView.class.getSimpleName(); + ViewPager.OnPageChangeListener, View.OnClickListener, View.OnTouchListener, + EmojiPageKeyboardView.OnKeyEventListener { + static final String TAG = EmojiPalettesView.class.getSimpleName(); private static final boolean DEBUG_PAGER = false; private final int mKeyBackgroundId; private final int mEmojiFunctionalKeyBackgroundId; - private final KeyboardLayoutSet mLayoutSet; private final ColorStateList mTabLabelColor; private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener; private EmojiPalettesAdapter mEmojiPalettesAdapter; + private final EmojiLayoutParams mEmojiLayoutParams; + private TextView mAlphabetKeyLeft; + private TextView mAlphabetKeyRight; private TabHost mTabHost; private ViewPager mEmojiPager; private int mCurrentPagerPosition = 0; @@ -116,7 +123,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange "places", "symbols", "emoticons" }; - private static final int[] sCategoryIcon = new int[] { + private static final int[] sCategoryIcon = { R.drawable.ic_emoji_recent_light, R.drawable.ic_emoji_people_light, R.drawable.ic_emoji_objects_light, @@ -126,6 +133,14 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange 0 }; private static final String[] sCategoryLabel = { null, null, null, null, null, null, ":-)" }; + private static final int[] sAccessibilityDescriptionResourceIdsForCategories = { + R.string.spoken_descrption_emoji_category_recents, + R.string.spoken_descrption_emoji_category_people, + R.string.spoken_descrption_emoji_category_objects, + R.string.spoken_descrption_emoji_category_nature, + R.string.spoken_descrption_emoji_category_places, + R.string.spoken_descrption_emoji_category_symbols, + R.string.spoken_descrption_emoji_category_emoticons }; private static final int[] sCategoryElementId = { KeyboardId.ELEMENT_EMOJI_RECENTS, KeyboardId.ELEMENT_EMOJI_CATEGORY1, @@ -135,6 +150,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange KeyboardId.ELEMENT_EMOJI_CATEGORY5, KeyboardId.ELEMENT_EMOJI_CATEGORY6 }; private final SharedPreferences mPrefs; + private final Resources mRes; private final int mMaxPageKeyCount; private final KeyboardLayoutSet mLayoutSet; private final HashMap<String, Integer> mCategoryNameToIdMap = CollectionUtils.newHashMap(); @@ -149,7 +165,8 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange public EmojiCategory(final SharedPreferences prefs, final Resources res, final KeyboardLayoutSet layoutSet) { mPrefs = prefs; - mMaxPageKeyCount = res.getInteger(R.integer.emoji_keyboard_max_key_count); + mRes = res; + mMaxPageKeyCount = res.getInteger(R.integer.config_emoji_keyboard_max_page_key_count); mLayoutSet = layoutSet; for (int i = 0; i < sCategoryName.length; ++i) { mCategoryNameToIdMap.put(sCategoryName[i], i); @@ -174,7 +191,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange .loadRecentKeys(mCategoryKeyboardMap.values()); } - private void addShownCategoryId(int categoryId) { + private void addShownCategoryId(final int categoryId) { // Load a keyboard of categoryId getKeyboard(categoryId, 0 /* cagetoryPageId */); final CategoryProperties properties = @@ -182,23 +199,27 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange mShownCategories.add(properties); } - public String getCategoryName(int categoryId, int categoryPageId) { + public String getCategoryName(final int categoryId, final int categoryPageId) { return sCategoryName[categoryId] + "-" + categoryPageId; } - public int getCategoryId(String name) { + public int getCategoryId(final String name) { final String[] strings = name.split("-"); return mCategoryNameToIdMap.get(strings[0]); } - public int getCategoryIcon(int categoryId) { + public int getCategoryIcon(final int categoryId) { return sCategoryIcon[categoryId]; } - public String getCategoryLabel(int categoryId) { + public String getCategoryLabel(final int categoryId) { return sCategoryLabel[categoryId]; } + public String getAccessibilityDescription(final int categoryId) { + return mRes.getString(sAccessibilityDescriptionResourceIdsForCategories[categoryId]); + } + public ArrayList<CategoryProperties> getShownCategories() { return mShownCategories; } @@ -211,7 +232,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange return getCategoryPageSize(mCurrentCategoryId); } - public int getCategoryPageSize(int categoryId) { + public int getCategoryPageSize(final int categoryId) { for (final CategoryProperties prop : mShownCategories) { if (prop.mCategoryId == categoryId) { return prop.mPageCount; @@ -222,12 +243,12 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange return 0; } - public void setCurrentCategoryId(int categoryId) { + public void setCurrentCategoryId(final int categoryId) { mCurrentCategoryId = categoryId; Settings.writeLastShownEmojiCategoryId(mPrefs, categoryId); } - public void setCurrentCategoryPageId(int id) { + public void setCurrentCategoryPageId(final int id) { mCurrentCategoryPageId = id; } @@ -244,7 +265,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange return mCurrentCategoryId == CATEGORY_ID_RECENTS; } - public int getTabIdFromCategoryId(int categoryId) { + public int getTabIdFromCategoryId(final int categoryId) { for (int i = 0; i < mShownCategories.size(); ++i) { if (mShownCategories.get(i).mCategoryId == categoryId) { return i; @@ -255,7 +276,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange } // Returns the view pager's page position for the categoryId - public int getPageIdFromCategoryId(int categoryId) { + public int getPageIdFromCategoryId(final int categoryId) { final int lastSavedCategoryPageId = Settings.readLastTypedEmojiCategoryPageId(mPrefs, categoryId); int sum = 0; @@ -274,7 +295,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange return getTabIdFromCategoryId(CATEGORY_ID_RECENTS); } - private int getCategoryPageCount(int categoryId) { + private int getCategoryPageCount(final int categoryId) { final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); return (keyboard.getKeys().length - 1) / mMaxPageKeyCount + 1; } @@ -283,9 +304,9 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange // position. The category page id is numbered in each category. And the view page position // is the position of the current shown page in the view pager which contains all pages of // all categories. - public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(int position) { + public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(final int position) { int sum = 0; - for (CategoryProperties properties : mShownCategories) { + for (final CategoryProperties properties : mShownCategories) { final int temp = sum; sum += properties.mPageCount; if (sum > position) { @@ -296,7 +317,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange } // Returns a keyboard from the view pager's page position. - public DynamicGridKeyboard getKeyboardFromPagePosition(int position) { + public DynamicGridKeyboard getKeyboardFromPagePosition(final int position) { final Pair<Integer, Integer> categoryAndId = getCategoryIdAndPageIdFromPagePosition(position); if (categoryAndId != null) { @@ -305,39 +326,41 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange return null; } - public DynamicGridKeyboard getKeyboard(int categoryId, int id) { - synchronized(mCategoryKeyboardMap) { - final long key = (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id; - final DynamicGridKeyboard kbd; - if (!mCategoryKeyboardMap.containsKey(key)) { - if (categoryId != CATEGORY_ID_RECENTS) { - final Keyboard keyboard = - mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); - final Key[][] sortedKeys = sortKeys(keyboard.getKeys(), mMaxPageKeyCount); - for (int i = 0; i < sortedKeys.length; ++i) { - final DynamicGridKeyboard tempKbd = new DynamicGridKeyboard(mPrefs, - mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), - mMaxPageKeyCount, categoryId, i /* categoryPageId */); - for (Key emojiKey : sortedKeys[i]) { - if (emojiKey == null) { - break; - } - tempKbd.addKeyLast(emojiKey); - } - mCategoryKeyboardMap.put((((long) categoryId) - << Constants.MAX_INT_BIT_COUNT) | i, tempKbd); + private static final Long getCategoryKeyboardMapKey(final int categoryId, final int id) { + return (((long) categoryId) << Constants.MAX_INT_BIT_COUNT) | id; + } + + public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) { + synchronized (mCategoryKeyboardMap) { + final Long categotyKeyboardMapKey = getCategoryKeyboardMapKey(categoryId, id); + if (mCategoryKeyboardMap.containsKey(categotyKeyboardMapKey)) { + return mCategoryKeyboardMap.get(categotyKeyboardMapKey); + } + + if (categoryId == CATEGORY_ID_RECENTS) { + final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs, + mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), + mMaxPageKeyCount, categoryId); + mCategoryKeyboardMap.put(categotyKeyboardMapKey, kbd); + return kbd; + } + + final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); + final Key[][] sortedKeys = sortKeysIntoPages(keyboard.getKeys(), mMaxPageKeyCount); + for (int pageId = 0; pageId < sortedKeys.length; ++pageId) { + final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs, + mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), + mMaxPageKeyCount, categoryId); + for (final Key emojiKey : sortedKeys[pageId]) { + if (emojiKey == null) { + break; } - kbd = mCategoryKeyboardMap.get(key); - } else { - kbd = new DynamicGridKeyboard(mPrefs, - mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), - mMaxPageKeyCount, categoryId, 0 /* categoryPageId */); - mCategoryKeyboardMap.put(key, kbd); + tempKeyboard.addKeyLast(emojiKey); } - } else { - kbd = mCategoryKeyboardMap.get(key); + mCategoryKeyboardMap.put( + getCategoryKeyboardMapKey(categoryId, pageId), tempKeyboard); } - return kbd; + return mCategoryKeyboardMap.get(categotyKeyboardMapKey); } } @@ -349,29 +372,31 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange return sum; } - private Key[][] sortKeys(Key[] inKeys, int maxPageCount) { - Key[] keys = Arrays.copyOf(inKeys, inKeys.length); - Arrays.sort(keys, 0, keys.length, new Comparator<Key>() { - @Override - public int compare(Key lhs, Key rhs) { - final Rect lHitBox = lhs.getHitBox(); - final Rect rHitBox = rhs.getHitBox(); - if (lHitBox.top < rHitBox.top) { - return -1; - } else if (lHitBox.top > rHitBox.top) { - return 1; - } - if (lHitBox.left < rHitBox.left) { - return -1; - } else if (lHitBox.left > rHitBox.left) { - return 1; - } - if (lhs.getCode() == rhs.getCode()) { - return 0; - } - return lhs.getCode() < rhs.getCode() ? -1 : 1; + private static Comparator<Key> EMOJI_KEY_COMPARATOR = new Comparator<Key>() { + @Override + public int compare(final Key lhs, final Key rhs) { + final Rect lHitBox = lhs.getHitBox(); + final Rect rHitBox = rhs.getHitBox(); + if (lHitBox.top < rHitBox.top) { + return -1; + } else if (lHitBox.top > rHitBox.top) { + return 1; + } + if (lHitBox.left < rHitBox.left) { + return -1; + } else if (lHitBox.left > rHitBox.left) { + return 1; } - }); + if (lhs.getCode() == rhs.getCode()) { + return 0; + } + return lhs.getCode() < rhs.getCode() ? -1 : 1; + } + }; + + private static Key[][] sortKeysIntoPages(final Key[] inKeys, final int maxPageCount) { + final Key[] keys = Arrays.copyOf(inKeys, inKeys.length); + Arrays.sort(keys, 0, keys.length, EMOJI_KEY_COMPARATOR); final int pageCount = (keys.length - 1) / maxPageCount + 1; final Key[][] retval = new Key[pageCount][maxPageCount]; for (int i = 0; i < keys.length; ++i) { @@ -404,12 +429,12 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( context, null /* editorInfo */); final Resources res = context.getResources(); - final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res); + mEmojiLayoutParams = new EmojiLayoutParams(res); builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype()); builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res), - emojiLp.mEmojiKeyboardHeight); - builder.setOptions(false, false, false /* lanuageSwitchKeyEnabled */); - mLayoutSet = builder.build(); + mEmojiLayoutParams.mEmojiKeyboardHeight); + builder.setOptions(false /* shortcutImeEnabled */, false /* showsVoiceInputKey */, + false /* languageSwitchKeyEnabled */); mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context), context.getResources(), builder.build()); mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener(context); @@ -423,7 +448,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange final int width = ResourceUtils.getDefaultKeyboardWidth(res) + getPaddingLeft() + getPaddingRight(); final int height = ResourceUtils.getDefaultKeyboardHeight(res) - + res.getDimensionPixelSize(R.dimen.suggestions_strip_height) + + res.getDimensionPixelSize(R.dimen.config_suggestions_strip_height) + getPaddingTop() + getPaddingBottom(); setMeasuredDimension(width, height); } @@ -436,12 +461,14 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate( R.layout.emoji_keyboard_tab_icon, null); iconView.setImageResource(mEmojiCategory.getCategoryIcon(categoryId)); + iconView.setContentDescription(mEmojiCategory.getAccessibilityDescription(categoryId)); tspec.setIndicator(iconView); } if (mEmojiCategory.getCategoryLabel(categoryId) != null) { final TextView textView = (TextView)LayoutInflater.from(getContext()).inflate( R.layout.emoji_keyboard_tab_label, null); textView.setText(mEmojiCategory.getCategoryLabel(categoryId)); + textView.setContentDescription(mEmojiCategory.getAccessibilityDescription(categoryId)); textView.setTextColor(mTabLabelColor); tspec.setIndicator(textView); } @@ -458,42 +485,60 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange mTabHost.setOnTabChangedListener(this); mTabHost.getTabWidget().setStripEnabled(true); - mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, mLayoutSet, this); + mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this); mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager); mEmojiPager.setAdapter(mEmojiPalettesAdapter); mEmojiPager.setOnPageChangeListener(this); mEmojiPager.setOffscreenPageLimit(0); - mEmojiPager.setPersistentDrawingCache(ViewPager.PERSISTENT_NO_CACHE); - final Resources res = getResources(); - final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res); - emojiLp.setPagerProperties(mEmojiPager); + mEmojiPager.setPersistentDrawingCache(PERSISTENT_NO_CACHE); + mEmojiLayoutParams.setPagerProperties(mEmojiPager); mEmojiCategoryPageIndicatorView = (EmojiCategoryPageIndicatorView)findViewById(R.id.emoji_category_page_id_view); - emojiLp.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView); + mEmojiLayoutParams.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView); setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true /* force */); final LinearLayout actionBar = (LinearLayout)findViewById(R.id.emoji_action_bar); - emojiLp.setActionBarProperties(actionBar); + mEmojiLayoutParams.setActionBarProperties(actionBar); + // deleteKey depends only on OnTouchListener. final ImageView deleteKey = (ImageView)findViewById(R.id.emoji_keyboard_delete); deleteKey.setTag(Constants.CODE_DELETE); deleteKey.setOnTouchListener(mDeleteKeyOnTouchListener); - final ImageView alphabetKey = (ImageView)findViewById(R.id.emoji_keyboard_alphabet); - alphabetKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); - alphabetKey.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL); - alphabetKey.setOnClickListener(this); + + // {@link #mAlphabetKeyLeft}, {@link #mAlphabetKeyRight, and spaceKey depend on + // {@link View.OnClickListener} as well as {@link View.OnTouchListener}. + // {@link View.OnTouchListener} is used as the trigger of key-press, while + // {@link View.OnClickListener} is used as the trigger of key-release which does not occur + // if the event is canceled by moving off the finger from the view. + // The text on alphabet keys are set at + // {@link #startEmojiPalettes(String,int,float,Typeface)}. + mAlphabetKeyLeft = (TextView)findViewById(R.id.emoji_keyboard_alphabet_left); + mAlphabetKeyLeft.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); + mAlphabetKeyLeft.setTag(Constants.CODE_ALPHA_FROM_EMOJI); + mAlphabetKeyLeft.setOnTouchListener(this); + mAlphabetKeyLeft.setOnClickListener(this); + mAlphabetKeyRight = (TextView)findViewById(R.id.emoji_keyboard_alphabet_right); + mAlphabetKeyRight.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); + mAlphabetKeyRight.setTag(Constants.CODE_ALPHA_FROM_EMOJI); + mAlphabetKeyRight.setOnTouchListener(this); + mAlphabetKeyRight.setOnClickListener(this); final ImageView spaceKey = (ImageView)findViewById(R.id.emoji_keyboard_space); spaceKey.setBackgroundResource(mKeyBackgroundId); spaceKey.setTag(Constants.CODE_SPACE); + spaceKey.setOnTouchListener(this); spaceKey.setOnClickListener(this); - emojiLp.setKeyProperties(spaceKey); - final ImageView alphabetKey2 = (ImageView)findViewById(R.id.emoji_keyboard_alphabet2); - alphabetKey2.setBackgroundResource(mEmojiFunctionalKeyBackgroundId); - alphabetKey2.setTag(Constants.CODE_SWITCH_ALPHA_SYMBOL); - alphabetKey2.setOnClickListener(this); + mEmojiLayoutParams.setKeyProperties(spaceKey); + } + + @Override + public boolean dispatchTouchEvent(final MotionEvent ev) { + // Add here to the stack trace to nail down the {@link IllegalArgumentException} exception + // in MotionEvent that sporadically happens. + // TODO: Remove this override method once the issue has been addressed. + return super.dispatchTouchEvent(ev); } @Override @@ -503,7 +548,6 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange updateEmojiCategoryPageIdView(); } - @Override public void onPageSelected(final int position) { final Pair<Integer, Integer> newPos = @@ -522,6 +566,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange @Override public void onPageScrolled(final int position, final float positionOffset, final int positionOffsetPixels) { + mEmojiPalettesAdapter.onPageScrolled(); final Pair<Integer, Integer> newPos = mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position); final int newCategoryId = newPos.first; @@ -541,41 +586,100 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange } } + /** + * Called from {@link EmojiPageKeyboardView} through {@link android.view.View.OnTouchListener} + * interface to handle touch events from View-based elements such as the space bar. + * Note that this method is used only for observing {@link MotionEvent#ACTION_DOWN} to trigger + * {@link KeyboardActionListener#onPressKey}. {@link KeyboardActionListener#onReleaseKey} will + * be covered by {@link #onClick} as long as the event is not canceled. + */ @Override - public void onClick(final View v) { - if (v.getTag() instanceof Integer) { - final int code = (Integer)v.getTag(); - registerCode(code); + public boolean onTouch(final View v, final MotionEvent event) { + if (event.getActionMasked() != MotionEvent.ACTION_DOWN) { + return false; + } + final Object tag = v.getTag(); + if (!(tag instanceof Integer)) { + return false; + } + final int code = (Integer) tag; + mKeyboardActionListener.onPressKey( + code, 0 /* repeatCount */, true /* isSinglePointer */); + // It's important to return false here. Otherwise, {@link #onClick} and touch-down visual + // feedback stop working. + return false; + } + + /** + * Called from {@link EmojiPageKeyboardView} through {@link android.view.View.OnClickListener} + * interface to handle non-canceled touch-up events from View-based elements such as the space + * bar. + */ + @Override + public void onClick(View v) { + final Object tag = v.getTag(); + if (!(tag instanceof Integer)) { return; } + final int code = (Integer) tag; + mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE, + false /* isKeyRepeat */); + mKeyboardActionListener.onReleaseKey(code, false /* withSliding */); } - private void registerCode(final int code) { + /** + * Called from {@link EmojiPageKeyboardView} through + * {@link com.android.inputmethod.keyboard.internal.EmojiPageKeyboardView.OnKeyEventListener} + * interface to handle touch events from non-View-based elements such as Emoji buttons. + */ + @Override + public void onPressKey(final Key key) { + final int code = key.getCode(); mKeyboardActionListener.onPressKey(code, 0 /* repeatCount */, true /* isSinglePointer */); - mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE); - mKeyboardActionListener.onReleaseKey(code, false /* withSliding */); } + /** + * Called from {@link EmojiPageKeyboardView} through + * {@link com.android.inputmethod.keyboard.internal.EmojiPageKeyboardView.OnKeyEventListener} + * interface to handle touch events from non-View-based elements such as Emoji buttons. + */ @Override - public void onKeyClick(final Key key) { + public void onReleaseKey(final Key key) { mEmojiPalettesAdapter.addRecentKey(key); mEmojiCategory.saveLastTypedCategoryPage(); final int code = key.getCode(); if (code == Constants.CODE_OUTPUT_TEXT) { mKeyboardActionListener.onTextInput(key.getOutputText()); - return; + } else { + mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE, + false /* isKeyRepeat */); } - registerCode(code); + mKeyboardActionListener.onReleaseKey(code, false /* withSliding */); } public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { - // TODO: + if (!enabled) return; + // TODO: Should use LAYER_TYPE_SOFTWARE when hardware acceleration is off? + setLayerType(LAYER_TYPE_HARDWARE, null); } - public void startEmojiPalettes() { + private static void setupAlphabetKey(final TextView alphabetKey, final String label, + final KeyDrawParams params) { + alphabetKey.setText(label); + alphabetKey.setTextColor(params.mTextColor); + alphabetKey.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mLabelSize); + alphabetKey.setTypeface(params.mTypeface); + } + + public void startEmojiPalettes(final String switchToAlphaLabel, + final KeyVisualAttributes keyVisualAttr) { if (DEBUG_PAGER) { Log.d(TAG, "allocate emoji palettes memory " + mCurrentPagerPosition); } + final KeyDrawParams params = new KeyDrawParams(); + params.updateParams(mEmojiLayoutParams.getActionBarHeight(), keyVisualAttr); + setupAlphabetKey(mAlphabetKeyLeft, switchToAlphaLabel, params); + setupAlphabetKey(mAlphabetKeyRight, switchToAlphaLabel, params); mEmojiPager.setAdapter(mEmojiPalettesAdapter); mEmojiPager.setCurrentItem(mCurrentPagerPosition); } @@ -628,16 +732,15 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange } private static class EmojiPalettesAdapter extends PagerAdapter { - private final ScrollKeyboardView.OnKeyClickListener mListener; + private final EmojiPageKeyboardView.OnKeyEventListener mListener; private final DynamicGridKeyboard mRecentsKeyboard; - private final SparseArray<ScrollKeyboardView> mActiveKeyboardViews = + private final SparseArray<EmojiPageKeyboardView> mActiveKeyboardViews = CollectionUtils.newSparseArray(); private final EmojiCategory mEmojiCategory; private int mActivePosition = 0; public EmojiPalettesAdapter(final EmojiCategory emojiCategory, - final KeyboardLayoutSet layoutSet, - final ScrollKeyboardView.OnKeyClickListener listener) { + final EmojiPageKeyboardView.OnKeyEventListener listener) { mEmojiCategory = emojiCategory; mListener = listener; mRecentsKeyboard = mEmojiCategory.getKeyboard(CATEGORY_ID_RECENTS, 0); @@ -665,17 +768,28 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange } } + public void onPageScrolled() { + // Make sure the delayed key-down event (highlight effect and haptic feedback) will be + // canceled. + final EmojiPageKeyboardView currentKeyboardView = + mActiveKeyboardViews.get(mActivePosition); + if (currentKeyboardView != null) { + currentKeyboardView.releaseCurrentKey(); + } + } + @Override public int getCount() { return mEmojiCategory.getTotalPageCountOfAllCategories(); } @Override - public void setPrimaryItem(final View container, final int position, final Object object) { + public void setPrimaryItem(final ViewGroup container, final int position, + final Object object) { if (mActivePosition == position) { return; } - final ScrollKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition); + final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(mActivePosition); if (oldKeyboardView != null) { oldKeyboardView.releaseCurrentKey(); oldKeyboardView.deallocateMemory(); @@ -688,7 +802,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange if (DEBUG_PAGER) { Log.d(TAG, "instantiate item: " + position); } - final ScrollKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position); + final EmojiPageKeyboardView oldKeyboardView = mActiveKeyboardViews.get(position); if (oldKeyboardView != null) { oldKeyboardView.deallocateMemory(); // This may be redundant but wanted to be safer.. @@ -697,18 +811,13 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange final Keyboard keyboard = mEmojiCategory.getKeyboardFromPagePosition(position); final LayoutInflater inflater = LayoutInflater.from(container.getContext()); - final View view = inflater.inflate( + final EmojiPageKeyboardView keyboardView = (EmojiPageKeyboardView)inflater.inflate( R.layout.emoji_keyboard_page, container, false /* attachToRoot */); - final ScrollKeyboardView keyboardView = (ScrollKeyboardView)view.findViewById( - R.id.emoji_keyboard_page); keyboardView.setKeyboard(keyboard); - keyboardView.setOnKeyClickListener(mListener); - final ScrollViewWithNotifier scrollView = (ScrollViewWithNotifier)view.findViewById( - R.id.emoji_keyboard_scroller); - keyboardView.setScrollView(scrollView); - container.addView(view); + keyboardView.setOnKeyEventListener(mListener); + container.addView(keyboardView); mActiveKeyboardViews.put(position, keyboardView); - return view; + return keyboardView; } @Override @@ -722,7 +831,7 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange if (DEBUG_PAGER) { Log.d(TAG, "destroy item: " + position + ", " + object.getClass().getSimpleName()); } - final ScrollKeyboardView keyboardView = mActiveKeyboardViews.get(position); + final EmojiPageKeyboardView keyboardView = mActiveKeyboardViews.get(position); if (keyboardView != null) { keyboardView.deallocateMemory(); mActiveKeyboardViews.remove(position); @@ -735,9 +844,8 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange } } - // TODO: Do the same things done in PointerTracker private static class DeleteKeyOnTouchListener implements OnTouchListener { - private static final long MAX_REPEAT_COUNT_TIME = 30 * DateUtils.SECOND_IN_MILLIS; + private static final long MAX_REPEAT_COUNT_TIME = TimeUnit.SECONDS.toMillis(30); private final int mDeleteKeyPressedBackgroundColor; private final long mKeyRepeatStartTimeout; private final long mKeyRepeatInterval; @@ -748,80 +856,117 @@ public final class EmojiPalettesView extends LinearLayout implements OnTabChange res.getColor(R.color.emoji_key_pressed_background_color); mKeyRepeatStartTimeout = res.getInteger(R.integer.config_key_repeat_start_timeout); mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); + mTimer = new CountDownTimer(MAX_REPEAT_COUNT_TIME, mKeyRepeatInterval) { + @Override + public void onTick(long millisUntilFinished) { + final long elapsed = MAX_REPEAT_COUNT_TIME - millisUntilFinished; + if (elapsed < mKeyRepeatStartTimeout) { + return; + } + onKeyRepeat(); + } + @Override + public void onFinish() { + onKeyRepeat(); + } + }; } + /** Key-repeat state. */ + private static final int KEY_REPEAT_STATE_INITIALIZED = 0; + // The key is touched but auto key-repeat is not started yet. + private static final int KEY_REPEAT_STATE_KEY_DOWN = 1; + // At least one key-repeat event has already been triggered and the key is not released. + private static final int KEY_REPEAT_STATE_KEY_REPEAT = 2; + private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER; - private DummyRepeatKeyRepeatTimer mTimer; - private synchronized void startRepeat() { - if (mTimer != null) { - abortRepeat(); - } - mTimer = new DummyRepeatKeyRepeatTimer(); - mTimer.start(); - } + // TODO: Do the same things done in PointerTracker + private final CountDownTimer mTimer; + private int mState = KEY_REPEAT_STATE_INITIALIZED; + private int mRepeatCount = 0; - private synchronized void abortRepeat() { - mTimer.abort(); - mTimer = null; + public void setKeyboardActionListener(final KeyboardActionListener listener) { + mKeyboardActionListener = listener; } - // TODO: Remove - // This function is mimicking the repeat code in PointerTracker. - // Specifically referring to PointerTracker#startRepeatKey and PointerTracker#onKeyRepeat. - private class DummyRepeatKeyRepeatTimer extends Thread { - public boolean mAborted = false; - - @Override - public void run() { - int repeatCount = 1; - int timeCount = 0; - while (timeCount < MAX_REPEAT_COUNT_TIME && !mAborted) { - if (timeCount > mKeyRepeatStartTimeout) { - pressDelete(repeatCount); - } - timeCount += mKeyRepeatInterval; - ++repeatCount; - try { - Thread.sleep(mKeyRepeatInterval); - } catch (InterruptedException e) { - } + @Override + public boolean onTouch(final View v, final MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + onTouchDown(v); + return true; + case MotionEvent.ACTION_MOVE: + final float x = event.getX(); + final float y = event.getY(); + if (x < 0.0f || v.getWidth() < x || y < 0.0f || v.getHeight() < y) { + // Stop generating key events once the finger moves away from the view area. + onTouchCanceled(v); } + return true; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + onTouchUp(v); + return true; } - - public void abort() { - mAborted = true; - } + return false; } - public void pressDelete(int repeatCount) { + private void handleKeyDown() { mKeyboardActionListener.onPressKey( - Constants.CODE_DELETE, repeatCount, true /* isSinglePointer */); - mKeyboardActionListener.onCodeInput( - Constants.CODE_DELETE, NOT_A_COORDINATE, NOT_A_COORDINATE); + Constants.CODE_DELETE, mRepeatCount, true /* isSinglePointer */); + } + + private void handleKeyUp() { + mKeyboardActionListener.onCodeInput(Constants.CODE_DELETE, + NOT_A_COORDINATE, NOT_A_COORDINATE, false /* isKeyRepeat */); mKeyboardActionListener.onReleaseKey( Constants.CODE_DELETE, false /* withSliding */); + ++mRepeatCount; } - public void setKeyboardActionListener(KeyboardActionListener listener) { - mKeyboardActionListener = listener; + private void onTouchDown(final View v) { + mTimer.cancel(); + mRepeatCount = 0; + handleKeyDown(); + v.setBackgroundColor(mDeleteKeyPressedBackgroundColor); + mState = KEY_REPEAT_STATE_KEY_DOWN; + mTimer.start(); } - @Override - public boolean onTouch(View v, MotionEvent event) { - switch(event.getAction()) { - case MotionEvent.ACTION_DOWN: - v.setBackgroundColor(mDeleteKeyPressedBackgroundColor); - pressDelete(0 /* repeatCount */); - startRepeat(); - return true; - case MotionEvent.ACTION_UP: - v.setBackgroundColor(0); - abortRepeat(); - return true; + private void onTouchUp(final View v) { + mTimer.cancel(); + if (mState == KEY_REPEAT_STATE_KEY_DOWN) { + handleKeyUp(); + } + v.setBackgroundColor(Color.TRANSPARENT); + mState = KEY_REPEAT_STATE_INITIALIZED; + } + + private void onTouchCanceled(final View v) { + mTimer.cancel(); + v.setBackgroundColor(Color.TRANSPARENT); + mState = KEY_REPEAT_STATE_INITIALIZED; + } + + // Called by {@link #mTimer} in the UI thread as an auto key-repeat signal. + private void onKeyRepeat() { + switch (mState) { + case KEY_REPEAT_STATE_INITIALIZED: + // Basically this should not happen. + break; + case KEY_REPEAT_STATE_KEY_DOWN: + // Do not call {@link #handleKeyDown} here because it has already been called + // in {@link #onTouchDown}. + handleKeyUp(); + mState = KEY_REPEAT_STATE_KEY_REPEAT; + break; + case KEY_REPEAT_STATE_KEY_REPEAT: + handleKeyDown(); + handleKeyUp(); + break; } - return false; } } } diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index f7ec9509d..816a94300 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -22,14 +22,11 @@ import static com.android.inputmethod.latin.Constants.CODE_SHIFT; import static com.android.inputmethod.latin.Constants.CODE_SWITCH_ALPHA_SYMBOL; import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED; -import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.TextUtils; -import android.util.Log; -import android.util.Xml; import com.android.inputmethod.keyboard.internal.KeyDrawParams; import com.android.inputmethod.keyboard.internal.KeySpecParser; @@ -43,9 +40,6 @@ import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.StringUtils; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - import java.util.Arrays; import java.util.Locale; @@ -53,8 +47,6 @@ import java.util.Locale; * Class for describing the position and characteristics of a single key in the keyboard. */ public class Key implements Comparable<Key> { - private static final String TAG = Key.class.getSimpleName(); - /** * The key code (unicode or custom code) that this key generates. */ @@ -84,10 +76,16 @@ public class Key implements Comparable<Key> { private static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800; private static final int LABEL_FLAGS_WITH_ICON_LEFT = 0x1000; private static final int LABEL_FLAGS_WITH_ICON_RIGHT = 0x2000; + // The bit to calculate the ratio of key label width against key width. If autoXScale bit is on + // and autoYScale bit is off, the key label may be shrunk only for X-direction. + // If both autoXScale and autoYScale bits are on, the key label text size may be auto scaled. private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000; - private static final int LABEL_FLAGS_PRESERVE_CASE = 0x8000; - private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x10000; - private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x20000; + private static final int LABEL_FLAGS_AUTO_Y_SCALE = 0x8000; + private static final int LABEL_FLAGS_AUTO_SCALE = LABEL_FLAGS_AUTO_X_SCALE + | LABEL_FLAGS_AUTO_Y_SCALE; + private static final int LABEL_FLAGS_PRESERVE_CASE = 0x10000; + private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000; + private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000; private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000; private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000; @@ -139,8 +137,6 @@ public class Key implements Comparable<Key> { private final OptionalAttributes mOptionalAttributes; - private static final int DEFAULT_TEXT_COLOR = 0xFFFFFFFF; - private static final class OptionalAttributes { /** Text to output when pressed. This can be multiple characters, like ".com" */ public final String mOutputText; @@ -185,22 +181,15 @@ public class Key implements Comparable<Key> { private boolean mEnabled = true; /** - * This constructor is being used only for keys in more keys keyboard. - */ - public Key(final KeyboardParams params, final MoreKeySpec moreKeySpec, final int x, final int y, - final int width, final int height, final int labelFlags) { - this(params, moreKeySpec.mLabel, null, moreKeySpec.mIconId, moreKeySpec.mCode, - moreKeySpec.mOutputText, x, y, width, height, labelFlags, BACKGROUND_TYPE_NORMAL); - } - - /** - * This constructor is being used only for key in popup suggestions pane. + * Constructor for a key on <code>MoreKeyKeyboard</code>, on <code>MoreSuggestions</code>, + * and in a <GridRows/>. */ - public Key(final KeyboardParams params, final String label, final String hintLabel, - final int iconId, final int code, final String outputText, final int x, final int y, - final int width, final int height, final int labelFlags, final int backgroundType) { - mHeight = height - params.mVerticalGap; - mWidth = width - params.mHorizontalGap; + public Key(final String label, final int iconId, final int code, final String outputText, + 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; mHintLabel = hintLabel; mLabelFlags = labelFlags; mBackgroundType = backgroundType; @@ -215,7 +204,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 + params.mHorizontalGap / 2; + mX = x + horizontalGap / 2; mY = y; mHitBox.set(x, y, x + width + 1, y + height); mKeyVisualAttributes = null; @@ -224,25 +213,22 @@ public class Key implements Comparable<Key> { } /** - * Create a key with the given top-left coordinate and extract its attributes from the XML - * parser. - * @param res resources associated with the caller's context + * Create a key with the given top-left coordinate and extract its attributes from a key + * specification string, Key attribute array, key style, and etc. + * + * @param keySpec the key specification. + * @param keyAttr the Key XML attributes array. + * @param keyStyle the {@link KeyStyle} of this key. * @param params the keyboard building parameters. * @param row the row that this key belongs to. row's x-coordinate will be the right edge of * this key. - * @param parser the XML parser containing the attributes for this key - * @throws XmlPullParserException */ - public Key(final Resources res, final KeyboardParams params, final KeyboardRow row, - final XmlPullParser parser) throws XmlPullParserException { + public Key(final String keySpec, final TypedArray keyAttr, final KeyStyle style, + final KeyboardParams params, final KeyboardRow row) { final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap; final int rowHeight = row.getRowHeight(); mHeight = rowHeight - params.mVerticalGap; - final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser), - R.styleable.Keyboard_Key); - - final KeyStyle style = params.mKeyStyles.getKeyStyle(keyAttr, parser); final float keyXPos = row.getKeyX(keyAttr); final float keyWidth = row.getKeyWidth(keyAttr, keyXPos); final int keyYPos = row.getKeyY(); @@ -264,12 +250,6 @@ public class Key implements Comparable<Key> { R.styleable.Keyboard_Key_visualInsetsLeft, baseWidth, baseWidth, 0)); final int visualInsetsRight = Math.round(keyAttr.getFraction( R.styleable.Keyboard_Key_visualInsetsRight, baseWidth, baseWidth, 0)); - mIconId = KeySpecParser.getIconId(style.getString(keyAttr, - R.styleable.Keyboard_Key_keyIcon)); - final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr, - R.styleable.Keyboard_Key_keyIconDisabled)); - final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr, - R.styleable.Keyboard_Key_keyIconPreview)); mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags) | row.getDefaultKeyLabelFlags(); @@ -281,19 +261,19 @@ public class Key implements Comparable<Key> { int moreKeysColumn = style.getInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMoreKeysKeyboardColumn); int value; - if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) { + if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) { moreKeysColumn = value & MORE_KEYS_COLUMN_MASK; } - if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) { + if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) { moreKeysColumn = MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER | (value & MORE_KEYS_COLUMN_MASK); } - if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) { + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) { moreKeysColumn |= MORE_KEYS_FLAGS_HAS_LABELS; } - if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) { + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) { moreKeysColumn |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS; } - if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) { + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) { moreKeysColumn |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY; } mMoreKeysColumnAndFlags = moreKeysColumn; @@ -305,21 +285,25 @@ public class Key implements Comparable<Key> { additionalMoreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys); } - moreKeys = KeySpecParser.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys); + moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys); if (moreKeys != null) { 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, params.mCodesSet); + mMoreKeys[i] = new MoreKeySpec(moreKeys[i], needsToUpperCase, locale); } } else { mMoreKeys = null; } mActionFlags = actionFlags; - final int code = KeySpecParser.parseCode(style.getString(keyAttr, - R.styleable.Keyboard_Key_code), params.mCodesSet, CODE_UNSPECIFIED); + mIconId = KeySpecParser.getIconId(keySpec); + final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr, + R.styleable.Keyboard_Key_keyIconDisabled)); + final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr, + R.styleable.Keyboard_Key_keyIconPreview)); + + final int code = KeySpecParser.getCode(keySpec); if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) { mLabel = params.mId.mCustomActionLabel; } else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) { @@ -328,25 +312,24 @@ public class Key implements Comparable<Key> { // code point nor as a surrogate pair. mLabel = new StringBuilder().appendCodePoint(code).toString(); } else { - mLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr, - R.styleable.Keyboard_Key_keyLabel), needsToUpperCase, locale); + mLabel = StringUtils.toUpperCaseOfStringForLocale( + KeySpecParser.getLabel(keySpec), needsToUpperCase, locale); } if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) { mHintLabel = null; } else { - mHintLabel = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr, + mHintLabel = StringUtils.toUpperCaseOfStringForLocale(style.getString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel), needsToUpperCase, locale); } - String outputText = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr, - R.styleable.Keyboard_Key_keyOutputText), needsToUpperCase, locale); + String outputText = StringUtils.toUpperCaseOfStringForLocale( + KeySpecParser.getOutputText(keySpec), needsToUpperCase, locale); // Choose the first letter of the label as primary code if not specified. if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText) && !TextUtils.isEmpty(mLabel)) { if (StringUtils.codePointCount(mLabel) == 1) { // Use the first letter of the hint label if shiftedLetterActivated flag is // specified. - if (hasShiftedLetterHint() && isShiftedLetterActivated() - && !TextUtils.isEmpty(mHintLabel)) { + if (hasShiftedLetterHint() && isShiftedLetterActivated()) { mCode = mHintLabel.codePointAt(0); } else { mCode = mLabel.codePointAt(0); @@ -365,24 +348,20 @@ public class Key implements Comparable<Key> { mCode = CODE_OUTPUT_TEXT; } } else { - mCode = KeySpecParser.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale); + mCode = StringUtils.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale); } - final int altCode = KeySpecParser.toUpperCaseOfCodeForLocale( - KeySpecParser.parseCode(style.getString(keyAttr, - R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED), - needsToUpperCase, locale); + final int altCodeInAttr = KeySpecParser.parseCode( + style.getString(keyAttr, R.styleable.Keyboard_Key_altCode), CODE_UNSPECIFIED); + final int altCode = StringUtils.toUpperCaseOfCodeForLocale( + altCodeInAttr, needsToUpperCase, locale); mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode, disabledIconId, previewIconId, visualInsetsLeft, visualInsetsRight); mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); - keyAttr.recycle(); mHashCode = computeHashCode(this); - if (hasShiftedLetterHint() && TextUtils.isEmpty(mHintLabel)) { - Log.w(TAG, "hasShiftedLetterHint specified without keyHintLabel: " + this); - } } /** - * Copy constructor. + * Copy constructor for DynamicGridKeyboard.GridKey. * * @param key the original key. */ @@ -604,22 +583,7 @@ public class Key implements Comparable<Key> { } public final int selectTextColor(final KeyDrawParams params) { - if (isShiftedLetterActivated()) { - return params.mTextInactivatedColor; - } - if (params.mTextColorStateList == null) { - return DEFAULT_TEXT_COLOR; - } - final int[] state; - // TODO: Hack!!!!!!!! Consider having a new attribute for the functional text labels. - // Currently, we distinguish "input key" from "functional key" by checking the - // length of the label( > 1) and "functional" attributes (= true). - if (mLabel != null && mLabel.length() > 1) { - state = getCurrentDrawableState(); - } else { - state = KEY_STATE_NORMAL; - } - return params.mTextColorStateList.getColorForState(state, DEFAULT_TEXT_COLOR); + return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor; } public final int selectHintTextSize(final KeyDrawParams params) { @@ -687,7 +651,8 @@ public class Key implements Comparable<Key> { } public final boolean hasShiftedLetterHint() { - return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0; + return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0 + && !TextUtils.isEmpty(mHintLabel); } public final boolean hasHintLabel() { @@ -702,12 +667,17 @@ public class Key implements Comparable<Key> { return (mLabelFlags & LABEL_FLAGS_WITH_ICON_RIGHT) != 0; } - public final boolean needsXScale() { + public final boolean needsAutoXScale() { return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0; } - public final boolean isShiftedLetterActivated() { - return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0; + public final boolean needsAutoScale() { + return (mLabelFlags & LABEL_FLAGS_AUTO_SCALE) == LABEL_FLAGS_AUTO_SCALE; + } + + private final boolean isShiftedLetterActivated() { + return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0 + && !TextUtils.isEmpty(mHintLabel); } public final int getMoreKeysColumn() { @@ -746,10 +716,14 @@ public class Key implements Comparable<Key> { return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED; } + public int getIconId() { + return mIconId; + } + public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) { final OptionalAttributes attrs = mOptionalAttributes; final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED; - final int iconId = mEnabled ? mIconId : disabledIconId; + final int iconId = mEnabled ? getIconId() : disabledIconId; final Drawable icon = iconSet.getIconDrawable(iconId); if (icon != null) { icon.setAlpha(alpha); @@ -761,7 +735,7 @@ public class Key implements Comparable<Key> { final OptionalAttributes attrs = mOptionalAttributes; final int previewIconId = (attrs != null) ? attrs.mPreviewIconId : ICON_UNDEFINED; return previewIconId != ICON_UNDEFINED - ? iconSet.getIconDrawable(previewIconId) : iconSet.getIconDrawable(mIconId); + ? iconSet.getIconDrawable(previewIconId) : iconSet.getIconDrawable(getIconId()); } public int getWidth() { @@ -928,9 +902,9 @@ public class Key implements Comparable<Key> { } public static class Spacer extends Key { - public Spacer(final Resources res, final KeyboardParams params, final KeyboardRow row, - final XmlPullParser parser) throws XmlPullParserException { - super(res, params, row, parser); + public Spacer(final TypedArray keyAttr, final KeyStyle keyStyle, + final KeyboardParams params, final KeyboardRow row) { + super(null /* keySpec */, keyAttr, keyStyle, params, row); } /** @@ -938,8 +912,9 @@ public class Key implements Comparable<Key> { */ protected Spacer(final KeyboardParams params, final int x, final int y, final int width, final int height) { - super(params, null, null, ICON_UNDEFINED, CODE_UNSPECIFIED, - null, x, y, width, height, 0, BACKGROUND_TYPE_EMPTY); + super(null /* label */, ICON_UNDEFINED, CODE_UNSPECIFIED, null /* outputText */, + null /* hintLabel */, 0 /* labelFlags */, BACKGROUND_TYPE_EMPTY, x, y, width, + height, params.mHorizontalGap, params.mVerticalGap); } } } diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java index befb6fa92..87368d4ef 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java +++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java @@ -16,9 +16,9 @@ package com.android.inputmethod.keyboard; -import com.android.inputmethod.latin.Constants; - - +/** + * This class handles key detection. + */ public class KeyDetector { private final int mKeyHysteresisDistanceSquared; private final int mKeyHysteresisDistanceForSlidingModifierSquared; @@ -27,31 +27,27 @@ public class KeyDetector { private int mCorrectionX; private int mCorrectionY; - /** - * This class handles key detection. - * - * @param keyHysteresisDistance if the pointer movement distance is smaller than this, the - * movement will not be handled as meaningful movement. The unit is pixel. - */ - public KeyDetector(float keyHysteresisDistance) { - this(keyHysteresisDistance, keyHysteresisDistance); + public KeyDetector() { + this(0.0f /* keyHysteresisDistance */, 0.0f /* keyHysteresisDistanceForSlidingModifier */); } /** - * This class handles key detection. + * Key detection object constructor with key hysteresis distances. * * @param keyHysteresisDistance if the pointer movement distance is smaller than this, the * movement will not be handled as meaningful movement. The unit is pixel. * @param keyHysteresisDistanceForSlidingModifier the same parameter for sliding input that * starts from a modifier key such as shift and symbols key. */ - public KeyDetector(float keyHysteresisDistance, float keyHysteresisDistanceForSlidingModifier) { + public KeyDetector(final float keyHysteresisDistance, + final float keyHysteresisDistanceForSlidingModifier) { mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance); mKeyHysteresisDistanceForSlidingModifierSquared = (int)( keyHysteresisDistanceForSlidingModifier * keyHysteresisDistanceForSlidingModifier); } - public void setKeyboard(Keyboard keyboard, float correctionX, float correctionY) { + public void setKeyboard(final Keyboard keyboard, final float correctionX, + final float correctionY) { if (keyboard == null) { throw new NullPointerException(); } @@ -60,28 +56,25 @@ public class KeyDetector { mKeyboard = keyboard; } - public int getKeyHysteresisDistanceSquared(boolean isSlidingFromModifier) { + public int getKeyHysteresisDistanceSquared(final boolean isSlidingFromModifier) { return isSlidingFromModifier ? mKeyHysteresisDistanceForSlidingModifierSquared : mKeyHysteresisDistanceSquared; } - public int getTouchX(int x) { + public int getTouchX(final int x) { return x + mCorrectionX; } // TODO: Remove vertical correction. - public int getTouchY(int y) { + public int getTouchY(final int y) { return y + mCorrectionY; } public Keyboard getKeyboard() { - if (mKeyboard == null) { - throw new IllegalStateException("keyboard isn't set"); - } return mKeyboard; } - public boolean alwaysAllowsSlidingInput() { + public boolean alwaysAllowsKeySelectionByDraggingFinger() { return false; } @@ -92,7 +85,10 @@ public class KeyDetector { * @param y The y-coordinate of a touch point * @return the key that the touch point hits. */ - public Key detectHitKey(int x, int y) { + public Key detectHitKey(final int x, final int y) { + if (mKeyboard == null) { + return null; + } final int touchX = getTouchX(x); final int touchY = getTouchY(y); @@ -117,20 +113,4 @@ public class KeyDetector { } return primaryKey; } - - public static String printableCode(Key key) { - return key != null ? Constants.printableCode(key.getCode()) : "none"; - } - - public static String printableCodes(int[] codes) { - final StringBuilder sb = new StringBuilder(); - boolean addDelimiter = false; - for (final int code : codes) { - if (code == Constants.NOT_A_CODE) break; - if (addDelimiter) sb.append(", "); - sb.append(Constants.printableCode(code)); - addDelimiter = true; - } - return "[" + sb + "]"; - } } diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java index bc1383aff..4fd3bac2f 100644 --- a/java/src/com/android/inputmethod/keyboard/Keyboard.java +++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java @@ -23,6 +23,7 @@ import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; import com.android.inputmethod.keyboard.internal.KeyboardParams; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.CoordinateUtils; /** * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard @@ -217,4 +218,20 @@ public class Keyboard { final int adjustedY = Math.max(0, Math.min(y, mOccupiedHeight - 1)); return mProximityInfo.getNearestKeys(adjustedX, adjustedY); } + + public int[] getCoordinates(final int[] codePoints) { + final int length = codePoints.length; + final int[] coordinates = CoordinateUtils.newCoordinateArray(length); + for (int i = 0; i < length; ++i) { + final Key key = getKey(codePoints[i]); + if (null != key) { + CoordinateUtils.setXYInArray(coordinates, i, + key.getX() + key.getWidth() / 2, key.getY() + key.getHeight() / 2); + } else { + CoordinateUtils.setXYInArray(coordinates, i, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } + } + return coordinates; + } } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java index dc760e685..c565866b7 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java @@ -53,8 +53,10 @@ public interface KeyboardActionListener { * {@link PointerTracker} or so, the value should be * {@link Constants#NOT_A_COORDINATE}.If it's called on insertion from the * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}. + * @param isKeyRepeat true if this is a key repeat, false otherwise */ - public void onCodeInput(int primaryCode, int x, int y); + // TODO: change this to send an Event object instead + public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat); /** * Sends a string of characters to the listener. @@ -107,7 +109,7 @@ public interface KeyboardActionListener { @Override public void onReleaseKey(int primaryCode, boolean withSliding) {} @Override - public void onCodeInput(int primaryCode, int x, int y) {} + public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat) {} @Override public void onTextInput(String text) {} @Override diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java index 736f13ed6..02beb3f11 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java @@ -70,8 +70,7 @@ public final class KeyboardId { public final int mElementId; private final EditorInfo mEditorInfo; public final boolean mClobberSettingsKey; - public final boolean mShortcutKeyEnabled; - public final boolean mShortcutKeyOnSymbols; + public final boolean mSupportsSwitchingToShortcutIme; public final boolean mLanguageSwitchKeyEnabled; public final String mCustomActionLabel; public final boolean mHasShortcutKey; @@ -87,17 +86,11 @@ public final class KeyboardId { mElementId = elementId; mEditorInfo = params.mEditorInfo; mClobberSettingsKey = params.mNoSettingsKey; - mShortcutKeyEnabled = params.mVoiceKeyEnabled; - mShortcutKeyOnSymbols = mShortcutKeyEnabled && !params.mVoiceKeyOnMain; + mSupportsSwitchingToShortcutIme = params.mSupportsSwitchingToShortcutIme; mLanguageSwitchKeyEnabled = params.mLanguageSwitchKeyEnabled; mCustomActionLabel = (mEditorInfo.actionLabel != null) ? mEditorInfo.actionLabel.toString() : null; - final boolean alphabetMayHaveShortcutKey = isAlphabetKeyboard(elementId) - && !mShortcutKeyOnSymbols; - final boolean symbolsMayHaveShortcutKey = (elementId == KeyboardId.ELEMENT_SYMBOLS) - && mShortcutKeyOnSymbols; - mHasShortcutKey = mShortcutKeyEnabled - && (alphabetMayHaveShortcutKey || symbolsMayHaveShortcutKey); + mHasShortcutKey = mSupportsSwitchingToShortcutIme && params.mShowsVoiceInputKey; mHashCode = computeHashCode(this); } @@ -110,8 +103,8 @@ public final class KeyboardId { id.mHeight, id.passwordInput(), id.mClobberSettingsKey, - id.mShortcutKeyEnabled, - id.mShortcutKeyOnSymbols, + id.mSupportsSwitchingToShortcutIme, + id.mHasShortcutKey, id.mLanguageSwitchKeyEnabled, id.isMultiLine(), id.imeAction(), @@ -131,8 +124,8 @@ public final class KeyboardId { && other.mHeight == mHeight && other.passwordInput() == passwordInput() && other.mClobberSettingsKey == mClobberSettingsKey - && other.mShortcutKeyEnabled == mShortcutKeyEnabled - && other.mShortcutKeyOnSymbols == mShortcutKeyOnSymbols + && other.mSupportsSwitchingToShortcutIme == mSupportsSwitchingToShortcutIme + && other.mHasShortcutKey == mHasShortcutKey && other.mLanguageSwitchKeyEnabled == mLanguageSwitchKeyEnabled && other.isMultiLine() == isMultiLine() && other.imeAction() == imeAction() @@ -186,21 +179,20 @@ 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%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), mWidth, mHeight, modeName(mMode), - imeAction(), - (navigateNext() ? "navigateNext" : ""), - (navigatePrevious() ? "navigatePrevious" : ""), + actionName(imeAction()), + (navigateNext() ? " navigateNext" : ""), + (navigatePrevious() ? " navigatePrevious" : ""), (mClobberSettingsKey ? " clobberSettingsKey" : ""), (passwordInput() ? " passwordInput" : ""), - (mShortcutKeyEnabled ? " shortcutKeyEnabled" : ""), - (mShortcutKeyOnSymbols ? " shortcutKeyOnSymbols" : ""), + (mSupportsSwitchingToShortcutIme ? " supportsSwitchingToShortcutIme" : ""), (mHasShortcutKey ? " hasShortcutKey" : ""), (mLanguageSwitchKeyEnabled ? " languageSwitchKeyEnabled" : ""), - (isMultiLine() ? "isMultiLine" : "") + (isMultiLine() ? " isMultiLine" : "") ); } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java index 1eccdf341..cde5091c4 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java @@ -20,7 +20,6 @@ import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII; import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE; import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT; import static com.android.inputmethod.latin.Constants.ImeOption.NO_SETTINGS_KEY; -import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE; import android.content.Context; import android.content.res.Resources; @@ -34,6 +33,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.compat.EditorInfoCompatUtils; +import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; import com.android.inputmethod.keyboard.internal.KeyboardBuilder; import com.android.inputmethod.keyboard.internal.KeyboardParams; import com.android.inputmethod.keyboard.internal.KeysCache; @@ -105,10 +105,10 @@ public final class KeyboardLayoutSet { int mMode; EditorInfo mEditorInfo; boolean mDisableTouchPositionCorrectionDataForTest; - boolean mVoiceKeyEnabled; - // TODO: Remove mVoiceKeyOnMain when it's certainly confirmed that we no longer show - // the voice input key on the symbol layout - boolean mVoiceKeyOnMain; + boolean mIsPasswordField; + boolean mSupportsSwitchingToShortcutIme; + boolean mShowsVoiceInputKey; + boolean mNoMicrophoneKey; boolean mNoSettingsKey; boolean mLanguageSwitchKeyEnabled; InputMethodSubtype mSubtype; @@ -221,16 +221,24 @@ public final class KeyboardLayoutSet { private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); - public Builder(final Context context, final EditorInfo editorInfo) { + public Builder(final Context context, final EditorInfo ei) { mContext = context; mPackageName = context.getPackageName(); mResources = context.getResources(); final Params params = mParams; + final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO; params.mMode = getKeyboardMode(editorInfo); - params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO; + params.mEditorInfo = editorInfo; + params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType); + @SuppressWarnings("deprecation") + final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions( + null, NO_MICROPHONE_COMPAT, editorInfo); + params.mNoMicrophoneKey = InputAttributes.inPrivateImeOptions( + mPackageName, NO_MICROPHONE, editorInfo) + || deprecatedNoMicrophone; params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( - mPackageName, NO_SETTINGS_KEY, params.mEditorInfo); + mPackageName, NO_SETTINGS_KEY, editorInfo); } public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) { @@ -240,7 +248,7 @@ public final class KeyboardLayoutSet { } public Builder setSubtype(final InputMethodSubtype subtype) { - final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE); + final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype); @SuppressWarnings("deprecation") final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( mPackageName, FORCE_ASCII, mParams.mEditorInfo); @@ -261,18 +269,11 @@ public final class KeyboardLayoutSet { return this; } - // TODO: Remove mVoiceKeyOnMain when it's certainly confirmed that we no longer show - // the voice input key on the symbol layout - public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain, - final boolean languageSwitchKeyEnabled) { - @SuppressWarnings("deprecation") - final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions( - null, NO_MICROPHONE_COMPAT, mParams.mEditorInfo); - final boolean noMicrophone = InputAttributes.inPrivateImeOptions( - mPackageName, NO_MICROPHONE, mParams.mEditorInfo) - || deprecatedNoMicrophone; - mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone; - mParams.mVoiceKeyOnMain = voiceKeyOnMain; + public Builder setOptions(final boolean isShortcutImeEnabled, + final boolean showsVoiceInputKey, final boolean languageSwitchKeyEnabled) { + mParams.mSupportsSwitchingToShortcutIme = + isShortcutImeEnabled && !mParams.mNoMicrophoneKey && !mParams.mIsPasswordField; + mParams.mShowsVoiceInputKey = showsVoiceInputKey; mParams.mLanguageSwitchKeyEnabled = languageSwitchKeyEnabled; return this; } @@ -368,9 +369,6 @@ public final class KeyboardLayoutSet { } private static int getKeyboardMode(final EditorInfo editorInfo) { - if (editorInfo == null) - return KeyboardId.MODE_TEXT; - final int inputType = editorInfo.inputType; final int variation = inputType & InputType.TYPE_MASK_VARIATION; diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java index 5abc9ab38..dcf7f7472 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java @@ -30,6 +30,7 @@ import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; import com.android.inputmethod.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException; import com.android.inputmethod.keyboard.internal.KeyboardState; +import com.android.inputmethod.keyboard.internal.KeyboardTextsSet; import com.android.inputmethod.latin.InputView; import com.android.inputmethod.latin.LatinIME; import com.android.inputmethod.latin.LatinImeLogger; @@ -37,35 +38,12 @@ import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; import com.android.inputmethod.latin.SubtypeSwitcher; import com.android.inputmethod.latin.WordComposer; -import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.settings.SettingsValues; import com.android.inputmethod.latin.utils.ResourceUtils; public final class KeyboardSwitcher implements KeyboardState.SwitchActions { private static final String TAG = KeyboardSwitcher.class.getSimpleName(); - static final class KeyboardTheme { - public final int mThemeId; - public final int mStyleId; - - // Note: The themeId should be aligned with "themeId" attribute of Keyboard style - // in values/style.xml. - public KeyboardTheme(final int themeId, final int styleId) { - mThemeId = themeId; - mStyleId = styleId; - } - } - - public static final int THEME_INDEX_ICS = 0; - public static final int THEME_INDEX_GB = 1; - public static final int THEME_INDEX_KLP = 2; - public static final int THEME_INDEX_DEFAULT = THEME_INDEX_KLP; - public static final KeyboardTheme[] KEYBOARD_THEMES = { - new KeyboardTheme(THEME_INDEX_ICS, R.style.KeyboardTheme_ICS), - new KeyboardTheme(THEME_INDEX_GB, R.style.KeyboardTheme_GB), - new KeyboardTheme(THEME_INDEX_KLP, R.style.KeyboardTheme_KLP), - }; - private SubtypeSwitcher mSubtypeSwitcher; private SharedPreferences mPrefs; @@ -74,18 +52,20 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { private MainKeyboardView mKeyboardView; private EmojiPalettesView mEmojiPalettesView; private LatinIME mLatinIME; - private Resources mResources; private boolean mIsHardwareAcceleratedDrawingEnabled; private KeyboardState mState; private KeyboardLayoutSet mKeyboardLayoutSet; + // TODO: The following {@link KeyboardTextsSet} should be in {@link KeyboardLayoutSet}. + private final KeyboardTextsSet mKeyboardTextsSet = new KeyboardTextsSet(); + private SettingsValues mCurrentSettingsValues; /** mIsAutoCorrectionActive indicates that auto corrected word will be input instead of * what user actually typed. */ private boolean mIsAutoCorrectionActive; - private KeyboardTheme mKeyboardTheme = KEYBOARD_THEMES[THEME_INDEX_DEFAULT]; + private KeyboardTheme mKeyboardTheme = KeyboardTheme.getDefaultKeyboardTheme(); private Context mThemeContext; private static final KeyboardSwitcher sInstance = new KeyboardSwitcher(); @@ -105,7 +85,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { private void initInternal(final LatinIME latinIme, final SharedPreferences prefs) { mLatinIME = latinIme; - mResources = latinIme.getResources(); mPrefs = prefs; mSubtypeSwitcher = SubtypeSwitcher.getInstance(); mState = new KeyboardState(this); @@ -115,25 +94,12 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { public void updateKeyboardTheme() { final boolean themeUpdated = updateKeyboardThemeAndContextThemeWrapper( - mLatinIME, getKeyboardTheme(mLatinIME, mPrefs)); + mLatinIME, KeyboardTheme.getKeyboardTheme(mPrefs)); if (themeUpdated && mKeyboardView != null) { mLatinIME.setInputView(onCreateInputView(mIsHardwareAcceleratedDrawingEnabled)); } } - private static KeyboardTheme getKeyboardTheme(final Context context, - final SharedPreferences prefs) { - final Resources res = context.getResources(); - final int index = Settings.readKeyboardThemeIndex(prefs, res); - if (index >= 0 && index < KEYBOARD_THEMES.length) { - return KEYBOARD_THEMES[index]; - } - final int defaultThemeIndex = Settings.resetAndGetDefaultKeyboardThemeIndex(prefs, res); - Log.w(TAG, "Illegal keyboard theme in preference: " + index + ", default to " - + defaultThemeIndex); - return KEYBOARD_THEMES[defaultThemeIndex]; - } - private boolean updateKeyboardThemeAndContextThemeWrapper(final Context context, final KeyboardTheme keyboardTheme) { if (mThemeContext == null || mKeyboardTheme.mThemeId != keyboardTheme.mThemeId) { @@ -145,7 +111,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { return false; } - public void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues) { + public void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues, + final int currentAutoCapsState, final int currentRecapitalizeState) { final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( mThemeContext, editorInfo); final Resources res = mThemeContext.getResources(); @@ -154,12 +121,14 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { builder.setKeyboardGeometry(keyboardWidth, keyboardHeight); builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype()); builder.setOptions( - settingsValues.isVoiceKeyEnabled(editorInfo), - true /* always show a voice key on the main keyboard */, + mSubtypeSwitcher.isShortcutImeEnabled(), + settingsValues.mShowsVoiceInputKey, settingsValues.isLanguageSwitchKeyEnabled()); mKeyboardLayoutSet = builder.build(); + mCurrentSettingsValues = settingsValues; try { - mState.onLoadKeyboard(); + mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState); + mKeyboardTextsSet.setLocale(mSubtypeSwitcher.getCurrentSubtypeLocale(), mThemeContext); } catch (KeyboardLayoutSetException e) { Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause()); LatinImeLogger.logOnException(e.mKeyboardId.toString(), e.getCause()); @@ -187,18 +156,25 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { final MainKeyboardView keyboardView = mKeyboardView; final Keyboard oldKeyboard = keyboardView.getKeyboard(); keyboardView.setKeyboard(keyboard); - mCurrentInputView.setKeyboardGeometry(keyboard.mTopPadding); + mCurrentInputView.setKeyboardTopPadding(keyboard.mTopPadding); keyboardView.setKeyPreviewPopupEnabled( - Settings.readKeyPreviewPopupEnabled(mPrefs, mResources), - Settings.readKeyPreviewPopupDismissDelay(mPrefs, mResources)); + mCurrentSettingsValues.mKeyPreviewPopupOn, + mCurrentSettingsValues.mKeyPreviewPopupDismissDelay); + keyboardView.setKeyPreviewAnimationParams( + mCurrentSettingsValues.mKeyPreviewShowUpStartScale, + mCurrentSettingsValues.mKeyPreviewShowUpDuration, + mCurrentSettingsValues.mKeyPreviewDismissEndScale, + mCurrentSettingsValues.mKeyPreviewDismissDuration); keyboardView.updateAutoCorrectionState(mIsAutoCorrectionActive); keyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady()); final boolean subtypeChanged = (oldKeyboard == null) || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale); - final boolean needsToDisplayLanguage = mSubtypeSwitcher.needsToDisplayLanguage( - keyboard.mId.mLocale); - keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, needsToDisplayLanguage, - RichInputMethodManager.getInstance().hasMultipleEnabledIMEsOrSubtypes(true)); + final int languageOnSpacebarFormatType = mSubtypeSwitcher.getLanguageOnSpacebarFormatType( + keyboard.mId.mSubtype); + final boolean hasMultipleEnabledIMEsOrSubtypes = RichInputMethodManager.getInstance() + .hasMultipleEnabledIMEsOrSubtypes(true /* shouldIncludeAuxiliarySubtypes */); + keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, languageOnSpacebarFormatType, + hasMultipleEnabledIMEsOrSubtypes); } public Keyboard getKeyboard() { @@ -208,30 +184,26 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { return null; } - /** - * Update keyboard shift state triggered by connected EditText status change. - */ - public void updateShiftState() { - mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState(), - mLatinIME.getCurrentRecapitalizeState()); - } - // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout // when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal(). - public void resetKeyboardStateToAlphabet() { - mState.onResetKeyboardStateToAlphabet(); + public void resetKeyboardStateToAlphabet(final int currentAutoCapsState, + final int currentRecapitalizeState) { + mState.onResetKeyboardStateToAlphabet(currentAutoCapsState, currentRecapitalizeState); } - public void onPressKey(final int code, final boolean isSinglePointer) { - mState.onPressKey(code, isSinglePointer, mLatinIME.getCurrentAutoCapsState()); + public void onPressKey(final int code, final boolean isSinglePointer, + final int currentAutoCapsState, final int currentRecapitalizeState) { + mState.onPressKey(code, isSinglePointer, currentAutoCapsState, currentRecapitalizeState); } - public void onReleaseKey(final int code, final boolean withSliding) { - mState.onReleaseKey(code, withSliding); + public void onReleaseKey(final int code, final boolean withSliding, + final int currentAutoCapsState, final int currentRecapitalizeState) { + mState.onReleaseKey(code, withSliding, currentAutoCapsState, currentRecapitalizeState); } - public void onFinishSlidingInput() { - mState.onFinishSlidingInput(); + public void onFinishSlidingInput(final int currentAutoCapsState, + final int currentRecapitalizeState) { + mState.onFinishSlidingInput(currentAutoCapsState, currentRecapitalizeState); } // Implements {@link KeyboardState.SwitchActions}. @@ -280,7 +252,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { @Override public void setEmojiKeyboard() { mMainKeyboardFrame.setVisibility(View.GONE); - mEmojiPalettesView.startEmojiPalettes(); + mEmojiPalettesView.startEmojiPalettes( + mKeyboardTextsSet.getText(KeyboardTextsSet.SWITCH_TO_ALPHA_KEY_LABEL), + mKeyboardView.getKeyVisualAttribute()); mEmojiPalettesView.setVisibility(View.VISIBLE); } @@ -290,11 +264,10 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_SYMBOLS_SHIFTED)); } - // Implements {@link KeyboardState.SwitchActions}. - @Override - public void requestUpdatingShiftState() { - mState.onUpdateShiftState(mLatinIME.getCurrentAutoCapsState(), - mLatinIME.getCurrentRecapitalizeState()); + // Future method for requesting an updating to the shift state. + public void requestUpdatingShiftState(final int currentAutoCapsState, + final int currentRecapitalizeState) { + mState.onUpdateShiftState(currentAutoCapsState, currentRecapitalizeState); } // Implements {@link KeyboardState.SwitchActions}. @@ -325,12 +298,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) { - mState.onCodeInput(code, mLatinIME.getCurrentAutoCapsState()); - } - - private boolean isShowingMainKeyboard() { - return null != mKeyboardView && mKeyboardView.isShown(); + public void onCodeInput(final int code, final int currentAutoCapsState, + final int currentRecapitalizeState) { + mState.onCodeInput(code, currentAutoCapsState, currentRecapitalizeState); } public boolean isShowingEmojiPalettes() { @@ -365,10 +335,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { } } - public boolean isShowingMainKeyboardOrEmojiPalettes() { - return isShowingMainKeyboard() || isShowingEmojiPalettes(); - } - public View onCreateInputView(final boolean isHardwareAcceleratedDrawingEnabled) { if (mKeyboardView != null) { mKeyboardView.closing(); diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java b/java/src/com/android/inputmethod/keyboard/KeyboardTheme.java new file mode 100644 index 000000000..4db72ad4d --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/KeyboardTheme.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.keyboard; + +import android.content.SharedPreferences; +import android.util.Log; + +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.settings.Settings; + +public final class KeyboardTheme { + private static final String TAG = KeyboardTheme.class.getSimpleName(); + + public static final int THEME_ID_ICS = 0; + public static final int THEME_ID_KLP = 2; + private static final int DEFAULT_THEME_ID = THEME_ID_KLP; + + private static final KeyboardTheme[] KEYBOARD_THEMES = { + new KeyboardTheme(THEME_ID_ICS, R.style.KeyboardTheme_ICS), + new KeyboardTheme(THEME_ID_KLP, R.style.KeyboardTheme_KLP), + }; + + public final int mThemeId; + public final int mStyleId; + + // Note: The themeId should be aligned with "themeId" attribute of Keyboard style + // in values/style.xml. + public KeyboardTheme(final int themeId, final int styleId) { + mThemeId = themeId; + mStyleId = styleId; + } + + private static KeyboardTheme searchKeyboardTheme(final int themeId) { + // TODO: This search algorithm isn't optimal if there are many themes. + for (final KeyboardTheme theme : KEYBOARD_THEMES) { + if (theme.mThemeId == themeId) { + return theme; + } + } + return null; + } + + public static KeyboardTheme getDefaultKeyboardTheme() { + return searchKeyboardTheme(DEFAULT_THEME_ID); + } + + public static KeyboardTheme getKeyboardTheme(final SharedPreferences prefs) { + final String themeIdString = prefs.getString(Settings.PREF_KEYBOARD_LAYOUT, null); + if (themeIdString == null) { + return getDefaultKeyboardTheme(); + } + try { + final int themeId = Integer.parseInt(themeIdString); + final KeyboardTheme theme = searchKeyboardTheme(themeId); + if (theme != null) { + return theme; + } + Log.w(TAG, "Unknown keyboard theme in preference: " + themeIdString); + } catch (final NumberFormatException e) { + Log.w(TAG, "Illegal keyboard theme in preference: " + themeIdString); + } + // Reset preference to default value. + final String defaultThemeIdString = Integer.toString(DEFAULT_THEME_ID); + prefs.edit().putString(Settings.PREF_KEYBOARD_LAYOUT, defaultThemeIdString).apply(); + return getDefaultKeyboardTheme(); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java index 5578713a0..18e51d392 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java @@ -113,9 +113,6 @@ public class KeyboardView extends View { private final Canvas mOffscreenCanvas = new Canvas(); private final Paint mPaint = new Paint(); private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); - private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' }; - private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' }; - public KeyboardView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.keyboardViewStyle); } @@ -149,6 +146,10 @@ public class KeyboardView extends View { mPaint.setAntiAlias(true); } + public KeyVisualAttributes getKeyVisualAttribute() { + return mKeyVisualAttributes; + } + private static void blendAlpha(final Paint paint, final int alpha) { final int color = paint.getColor(); paint.setARGB((paint.getAlpha() * alpha) / Constants.Color.ALPHA_OPAQUE, @@ -322,7 +323,7 @@ public class KeyboardView extends View { params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE; if (!key.isSpacer()) { - onDrawKeyBackground(key, canvas); + onDrawKeyBackground(key, canvas, mKeyBackground); } onDrawKeyTopVisuals(key, canvas, paint, params); @@ -330,14 +331,14 @@ public class KeyboardView extends View { } // Draw key background. - protected void onDrawKeyBackground(final Key key, final Canvas canvas) { + protected void onDrawKeyBackground(final Key key, final Canvas canvas, + final Drawable background) { final Rect padding = mKeyBackgroundPadding; final int bgWidth = key.getDrawWidth() + padding.left + padding.right; final int bgHeight = key.getHeight() + padding.top + padding.bottom; final int bgX = -padding.left; final int bgY = -padding.top; final int[] drawableState = key.getCurrentDrawableState(); - final Drawable background = mKeyBackground; background.setState(drawableState); final Rect bounds = background.getBounds(); if (bgWidth != bounds.right || bgHeight != bounds.bottom) { @@ -370,10 +371,8 @@ public class KeyboardView extends View { if (label != null) { paint.setTypeface(key.selectTypeface(params)); paint.setTextSize(key.selectTextSize(params)); - final float labelCharHeight = TypefaceUtils.getCharHeight( - KEY_LABEL_REFERENCE_CHAR, paint); - final float labelCharWidth = TypefaceUtils.getCharWidth( - KEY_LABEL_REFERENCE_CHAR, paint); + final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint); + final float labelCharWidth = TypefaceUtils.getReferenceCharWidth(paint); // Vertical label text alignment. final float baseline = centerY + labelCharHeight / 2.0f; @@ -391,12 +390,12 @@ public class KeyboardView extends View { positionX = centerX - labelCharWidth * 7.0f / 4.0f; paint.setTextAlign(Align.LEFT); } else if (key.hasLabelWithIconLeft() && icon != null) { - labelWidth = TypefaceUtils.getLabelWidth(label, paint) + icon.getIntrinsicWidth() + labelWidth = TypefaceUtils.getStringWidth(label, paint) + icon.getIntrinsicWidth() + LABEL_ICON_MARGIN * keyWidth; positionX = centerX + labelWidth / 2.0f; paint.setTextAlign(Align.RIGHT); } else if (key.hasLabelWithIconRight() && icon != null) { - labelWidth = TypefaceUtils.getLabelWidth(label, paint) + icon.getIntrinsicWidth() + labelWidth = TypefaceUtils.getStringWidth(label, paint) + icon.getIntrinsicWidth() + LABEL_ICON_MARGIN * keyWidth; positionX = centerX - labelWidth / 2.0f; paint.setTextAlign(Align.LEFT); @@ -404,9 +403,15 @@ public class KeyboardView extends View { positionX = centerX; paint.setTextAlign(Align.CENTER); } - if (key.needsXScale()) { - paint.setTextScaleX(Math.min(1.0f, - (keyWidth * MAX_LABEL_RATIO) / TypefaceUtils.getLabelWidth(label, paint))); + if (key.needsAutoXScale()) { + final float ratio = Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / + TypefaceUtils.getStringWidth(label, paint)); + if (key.needsAutoScale()) { + final float autoSize = paint.getTextSize() * ratio; + paint.setTextSize(autoSize); + } else { + paint.setTextScaleX(ratio); + } } paint.setColor(key.selectTextColor(params)); @@ -451,36 +456,35 @@ public class KeyboardView extends View { // TODO: Should add a way to specify type face for hint letters paint.setTypeface(Typeface.DEFAULT_BOLD); blendAlpha(paint, params.mAnimAlpha); + final float labelCharHeight = TypefaceUtils.getReferenceCharHeight(paint); + final float labelCharWidth = TypefaceUtils.getReferenceCharWidth(paint); + final KeyVisualAttributes visualAttr = key.getVisualAttributes(); + final float adjustmentY = (visualAttr == null) ? 0.0f + : visualAttr.mHintLabelVerticalAdjustment * labelCharHeight; final float hintX, hintY; if (key.hasHintLabel()) { // The hint label is placed just right of the key label. Used mainly on // "phone number" layout. // TODO: Generalize the following calculations. - hintX = positionX - + TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2.0f; - hintY = centerY - + TypefaceUtils.getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; + hintX = positionX + labelCharWidth * 2.0f; + hintY = centerY + labelCharHeight / 2.0f; paint.setTextAlign(Align.LEFT); } else if (key.hasShiftedLetterHint()) { // The hint label is placed at top-right corner of the key. Used mainly on tablet. - hintX = keyWidth - mKeyShiftedLetterHintPadding - - TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; + hintX = keyWidth - mKeyShiftedLetterHintPadding - labelCharWidth / 2.0f; paint.getFontMetrics(mFontMetrics); hintY = -mFontMetrics.top; paint.setTextAlign(Align.CENTER); } else { // key.hasHintLetter() // The hint letter is placed at top-right corner of the key. Used mainly on phone. - final float keyNumericHintLabelReferenceCharWidth = - TypefaceUtils.getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint); - final float keyHintLabelStringWidth = - TypefaceUtils.getStringWidth(hintLabel, paint); + final float hintDigitWidth = TypefaceUtils.getReferenceDigitWidth(paint); + final float hintLabelWidth = TypefaceUtils.getStringWidth(hintLabel, paint); hintX = keyWidth - mKeyHintLetterPadding - - Math.max(keyNumericHintLabelReferenceCharWidth, keyHintLabelStringWidth) - / 2.0f; + - Math.max(hintDigitWidth, hintLabelWidth) / 2.0f; hintY = -paint.ascent(); paint.setTextAlign(Align.CENTER); } - canvas.drawText(hintLabel, 0, hintLabel.length(), hintX, hintY, paint); + canvas.drawText(hintLabel, 0, hintLabel.length(), hintX, hintY + adjustmentY, paint); if (LatinImeLogger.sVISUALDEBUG) { final Paint line = new Paint(); @@ -530,7 +534,7 @@ public class KeyboardView extends View { paint.setColor(params.mHintLabelColor); paint.setTextAlign(Align.CENTER); final float hintX = keyWidth - mKeyHintLetterPadding - - TypefaceUtils.getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2.0f; + - TypefaceUtils.getReferenceCharWidth(paint) / 2.0f; final float hintY = keyHeight - mKeyPopupHintLetterPadding; canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint); @@ -582,6 +586,7 @@ public class KeyboardView extends View { paint.setTypeface(mKeyDrawParams.mTypeface); paint.setTextSize(mKeyDrawParams.mLabelSize); } else { + paint.setColor(key.selectTextColor(mKeyDrawParams)); paint.setTypeface(key.selectTypeface(mKeyDrawParams)); paint.setTextSize(key.selectTextSize(mKeyDrawParams)); } diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 13db47004..1cafd4162 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -28,18 +28,12 @@ import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Typeface; import android.graphics.drawable.Drawable; -import android.os.Message; -import android.os.SystemClock; import android.preference.PreferenceManager; import android.util.AttributeSet; -import android.util.DisplayMetrics; import android.util.Log; -import android.util.SparseArray; -import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.inputmethod.InputMethodSubtype; import android.widget.TextView; @@ -47,15 +41,17 @@ import android.widget.TextView; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.annotations.ExternallyReferenced; -import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy; -import com.android.inputmethod.keyboard.PointerTracker.TimerProxy; -import com.android.inputmethod.keyboard.internal.GestureFloatingPreviewText; -import com.android.inputmethod.keyboard.internal.GestureTrailsPreview; +import com.android.inputmethod.keyboard.internal.DrawingHandler; +import com.android.inputmethod.keyboard.internal.DrawingPreviewPlacerView; +import com.android.inputmethod.keyboard.internal.GestureFloatingTextDrawingPreview; +import com.android.inputmethod.keyboard.internal.GestureTrailsDrawingPreview; import com.android.inputmethod.keyboard.internal.KeyDrawParams; +import com.android.inputmethod.keyboard.internal.KeyPreviewChoreographer; import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams; +import com.android.inputmethod.keyboard.internal.LanguageOnSpacebarHelper; import com.android.inputmethod.keyboard.internal.NonDistinctMultitouchHelper; -import com.android.inputmethod.keyboard.internal.PreviewPlacerView; -import com.android.inputmethod.keyboard.internal.SlidingKeyInputPreview; +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.LatinImeLogger; import com.android.inputmethod.latin.R; @@ -64,11 +60,9 @@ import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.latin.settings.DebugSettings; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.CoordinateUtils; -import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper; -import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; +import com.android.inputmethod.latin.utils.SpacebarLanguageUtils; import com.android.inputmethod.latin.utils.TypefaceUtils; import com.android.inputmethod.latin.utils.UsabilityStudyLogUtils; -import com.android.inputmethod.latin.utils.ViewLayoutUtils; import com.android.inputmethod.research.ResearchLogger; import java.util.WeakHashMap; @@ -78,9 +72,10 @@ import java.util.WeakHashMap; * * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedEnabled * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedIcon - * @attr ref R.styleable#MainKeyboardView_spacebarTextRatio - * @attr ref R.styleable#MainKeyboardView_spacebarTextColor - * @attr ref R.styleable#MainKeyboardView_spacebarTextShadowColor + * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarTextRatio + * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarTextColor + * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarTextShadowColor + * @attr ref R.styleable#MainKeyboardView_spacebarBackground * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFinalAlpha * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFadeoutAnimator * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator @@ -88,7 +83,7 @@ import java.util.WeakHashMap; * @attr ref R.styleable#MainKeyboardView_keyHysteresisDistance * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdTime * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdDistance - * @attr ref R.styleable#MainKeyboardView_slidingKeyInputEnable + * @attr ref R.styleable#MainKeyboardView_keySelectionByDraggingFinger * @attr ref R.styleable#MainKeyboardView_keyRepeatStartTimeout * @attr ref R.styleable#MainKeyboardView_keyRepeatInterval * @attr ref R.styleable#MainKeyboardView_longPressKeyTimeout @@ -114,26 +109,27 @@ import java.util.WeakHashMap; * @attr ref R.styleable#MainKeyboardView_gestureRecognitionSpeedThreshold * @attr ref R.styleable#MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration */ -public final class MainKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler, - PointerTracker.DrawingProxy, MoreKeysPanel.Controller { +public final class MainKeyboardView extends KeyboardView implements PointerTracker.DrawingProxy, + MoreKeysPanel.Controller, DrawingHandler.Callbacks, TimerHandler.Callbacks { private static final String TAG = MainKeyboardView.class.getSimpleName(); /** Listener for {@link KeyboardActionListener}. */ private KeyboardActionListener mKeyboardActionListener; - /* Space key and its icons */ + /* Space key and its icon and background. */ private Key mSpaceKey; - private Drawable mSpaceIcon; + private Drawable mSpacebarIcon; + private final Drawable mSpacebarBackground; // Stuff to draw language name on spacebar. private final int mLanguageOnSpacebarFinalAlpha; private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator; - private boolean mNeedsToDisplayLanguage; + private int mLanguageOnSpacebarFormatType; private boolean mHasMultipleEnabledIMEsOrSubtypes; private int mLanguageOnSpacebarAnimAlpha = Constants.Color.ALPHA_OPAQUE; - private final float mSpacebarTextRatio; - private float mSpacebarTextSize; - private final int mSpacebarTextColor; - private final int mSpacebarTextShadowColor; + private final float mLanguageOnSpacebarTextRatio; + private float mLanguageOnSpacebarTextSize; + private final int mLanguageOnSpacebarTextColor; + private final int mLanguageOnSpacebarTextShadowColor; // The minimum x-scale to fit the language name on spacebar. private static final float MINIMUM_XSCALE_OF_LANGUAGE_NAME = 0.8f; // Stuff to draw auto correction LED on spacebar. @@ -143,25 +139,21 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack private static final int SPACE_LED_LENGTH_PERCENT = 80; // Stuff to draw altCodeWhileTyping keys. - private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator; - private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator; + private final ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator; + private final ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator; private int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE; - // Preview placer view - private final PreviewPlacerView mPreviewPlacerView; + // Drawing preview placer view + private final DrawingPreviewPlacerView mDrawingPreviewPlacerView; private final int[] mOriginCoords = CoordinateUtils.newInstance(); - private final GestureFloatingPreviewText mGestureFloatingPreviewText; - private final GestureTrailsPreview mGestureTrailsPreview; - private final SlidingKeyInputPreview mSlidingKeyInputPreview; + private final GestureFloatingTextDrawingPreview mGestureFloatingTextDrawingPreview; + private final GestureTrailsDrawingPreview mGestureTrailsDrawingPreview; + private final SlidingKeyInputDrawingPreview mSlidingKeyInputDrawingPreview; // Key preview - private final int mKeyPreviewLayoutId; - private final int mKeyPreviewOffset; - private final int mKeyPreviewHeight; - private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray(); - private final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams(); - private boolean mShowKeyPreviewPopup = true; - private int mKeyPreviewLingerTimeout; + private static final boolean FADE_OUT_KEY_TOP_LETTER_WHEN_KEY_IS_PRESSED = false; + private final KeyPreviewDrawParams mKeyPreviewDrawParams; + private final KeyPreviewChoreographer mKeyPreviewChoreographer; // More keys keyboard private final Paint mBackgroundDimAlphaPaint = new Paint(); @@ -178,244 +170,14 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack // TODO: Make this parameter customizable by user via settings. private int mGestureFloatingPreviewTextLingerTimeout; - private KeyDetector mKeyDetector; + private final KeyDetector mKeyDetector; private final NonDistinctMultitouchHelper mNonDistinctMultitouchHelper; - private final KeyTimerHandler mKeyTimerHandler; + private final TimerHandler mKeyTimerHandler; private final int mLanguageOnSpacebarHorizontalMargin; - private static final class KeyTimerHandler extends StaticInnerHandlerWrapper<MainKeyboardView> - implements TimerProxy { - private static final int MSG_TYPING_STATE_EXPIRED = 0; - private static final int MSG_REPEAT_KEY = 1; - private static final int MSG_LONGPRESS_KEY = 2; - private static final int MSG_DOUBLE_TAP_SHIFT_KEY = 3; - private static final int MSG_UPDATE_BATCH_INPUT = 4; - - private final int mIgnoreAltCodeKeyTimeout; - private final int mGestureRecognitionUpdateTime; - - public KeyTimerHandler(final MainKeyboardView outerInstance, - final TypedArray mainKeyboardViewAttr) { - super(outerInstance); - - mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0); - mGestureRecognitionUpdateTime = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureRecognitionUpdateTime, 0); - } - - @Override - public void handleMessage(final Message msg) { - final MainKeyboardView keyboardView = getOuterInstance(); - if (keyboardView == null) { - return; - } - final PointerTracker tracker = (PointerTracker) msg.obj; - switch (msg.what) { - case MSG_TYPING_STATE_EXPIRED: - startWhileTypingFadeinAnimation(keyboardView); - break; - case MSG_REPEAT_KEY: - tracker.onKeyRepeat(msg.arg1 /* code */, msg.arg2 /* repeatCount */); - break; - case MSG_LONGPRESS_KEY: - keyboardView.onLongPress(tracker); - break; - case MSG_UPDATE_BATCH_INPUT: - tracker.updateBatchInputByTimer(SystemClock.uptimeMillis()); - startUpdateBatchInputTimer(tracker); - break; - } - } - - @Override - public void startKeyRepeatTimer(final PointerTracker tracker, final int repeatCount, - final int delay) { - final Key key = tracker.getKey(); - if (key == null || delay == 0) { - return; - } - sendMessageDelayed( - obtainMessage(MSG_REPEAT_KEY, key.getCode(), repeatCount, tracker), delay); - } - - public void cancelKeyRepeatTimer() { - removeMessages(MSG_REPEAT_KEY); - } - - // TODO: Suppress layout changes in key repeat mode - public boolean isInKeyRepeat() { - return hasMessages(MSG_REPEAT_KEY); - } - - @Override - public void startLongPressTimer(final PointerTracker tracker, final int delay) { - cancelLongPressTimer(); - if (delay <= 0) return; - sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay); - } - - @Override - public void cancelLongPressTimer() { - removeMessages(MSG_LONGPRESS_KEY); - } - - private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel, - final ObjectAnimator animatorToStart) { - if (animatorToCancel == null || animatorToStart == null) { - // TODO: Stop using null as a no-operation animator. - return; - } - float startFraction = 0.0f; - if (animatorToCancel.isStarted()) { - animatorToCancel.cancel(); - startFraction = 1.0f - animatorToCancel.getAnimatedFraction(); - } - final long startTime = (long)(animatorToStart.getDuration() * startFraction); - animatorToStart.start(); - animatorToStart.setCurrentPlayTime(startTime); - } - - private static void startWhileTypingFadeinAnimation(final MainKeyboardView keyboardView) { - cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator, - keyboardView.mAltCodeKeyWhileTypingFadeinAnimator); - } - - private static void startWhileTypingFadeoutAnimation(final MainKeyboardView keyboardView) { - cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator, - keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator); - } - - @Override - public void startTypingStateTimer(final Key typedKey) { - if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) { - return; - } - - final boolean isTyping = isTypingState(); - removeMessages(MSG_TYPING_STATE_EXPIRED); - final MainKeyboardView keyboardView = getOuterInstance(); - - // When user hits the space or the enter key, just cancel the while-typing timer. - final int typedCode = typedKey.getCode(); - if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) { - if (isTyping) { - startWhileTypingFadeinAnimation(keyboardView); - } - return; - } - - sendMessageDelayed( - obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout); - if (isTyping) { - return; - } - startWhileTypingFadeoutAnimation(keyboardView); - } - - @Override - public boolean isTypingState() { - return hasMessages(MSG_TYPING_STATE_EXPIRED); - } - - @Override - public void startDoubleTapShiftKeyTimer() { - sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP_SHIFT_KEY), - ViewConfiguration.getDoubleTapTimeout()); - } - - @Override - public void cancelDoubleTapShiftKeyTimer() { - removeMessages(MSG_DOUBLE_TAP_SHIFT_KEY); - } - - @Override - public boolean isInDoubleTapShiftKeyTimeout() { - return hasMessages(MSG_DOUBLE_TAP_SHIFT_KEY); - } - - @Override - public void cancelKeyTimers() { - cancelKeyRepeatTimer(); - cancelLongPressTimer(); - } - - @Override - public void startUpdateBatchInputTimer(final PointerTracker tracker) { - if (mGestureRecognitionUpdateTime <= 0) { - return; - } - removeMessages(MSG_UPDATE_BATCH_INPUT, tracker); - sendMessageDelayed(obtainMessage(MSG_UPDATE_BATCH_INPUT, tracker), - mGestureRecognitionUpdateTime); - } - - @Override - public void cancelUpdateBatchInputTimer(final PointerTracker tracker) { - removeMessages(MSG_UPDATE_BATCH_INPUT, tracker); - } - - @Override - public void cancelAllUpdateBatchInputTimers() { - removeMessages(MSG_UPDATE_BATCH_INPUT); - } - - public void cancelAllMessages() { - cancelKeyTimers(); - cancelAllUpdateBatchInputTimers(); - } - } - - private final DrawingHandler mDrawingHandler = new DrawingHandler(this); - - public static class DrawingHandler extends StaticInnerHandlerWrapper<MainKeyboardView> { - private static final int MSG_DISMISS_KEY_PREVIEW = 0; - private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; - - public DrawingHandler(final MainKeyboardView outerInstance) { - super(outerInstance); - } - - @Override - public void handleMessage(final Message msg) { - final MainKeyboardView mainKeyboardView = getOuterInstance(); - if (mainKeyboardView == null) return; - final PointerTracker tracker = (PointerTracker) msg.obj; - switch (msg.what) { - case MSG_DISMISS_KEY_PREVIEW: - final TextView previewText = mainKeyboardView.mKeyPreviewTexts.get( - tracker.mPointerId); - if (previewText != null) { - previewText.setVisibility(INVISIBLE); - } - break; - case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT: - mainKeyboardView.showGestureFloatingPreviewText(SuggestedWords.EMPTY); - break; - } - } - - public void dismissKeyPreview(final long delay, final PointerTracker tracker) { - sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay); - } - - public void cancelDismissKeyPreview(final PointerTracker tracker) { - removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker); - } - - private void cancelAllDismissKeyPreviews() { - removeMessages(MSG_DISMISS_KEY_PREVIEW); - } - - public void dismissGestureFloatingPreviewText(final long delay) { - sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT), delay); - } - - public void cancelAllMessages() { - cancelAllDismissKeyPreviews(); - } - } + private final DrawingHandler mDrawingHandler = + new DrawingHandler(this); public MainKeyboardView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.mainKeyboardViewStyle); @@ -424,7 +186,26 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack public MainKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); - PointerTracker.init(getResources()); + mDrawingPreviewPlacerView = new DrawingPreviewPlacerView(context, attrs); + + final TypedArray mainKeyboardViewAttr = context.obtainStyledAttributes( + attrs, R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView); + final int ignoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0); + final int gestureRecognitionUpdateTime = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureRecognitionUpdateTime, 0); + mKeyTimerHandler = new TimerHandler( + this, ignoreAltCodeKeyTimeout, gestureRecognitionUpdateTime); + + final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f); + final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f); + mKeyDetector = new KeyDetector( + keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier); + + PointerTracker.init(mainKeyboardViewAttr, mKeyTimerHandler, this /* DrawingProxy */); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final boolean forceNonDistinctMultitouch = prefs.getBoolean( DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH, false); @@ -434,24 +215,22 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mNonDistinctMultitouchHelper = hasDistinctMultitouch ? null : new NonDistinctMultitouchHelper(); - mPreviewPlacerView = new PreviewPlacerView(context, attrs); - - final TypedArray mainKeyboardViewAttr = context.obtainStyledAttributes( - attrs, R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView); final int backgroundDimAlpha = mainKeyboardViewAttr.getInt( R.styleable.MainKeyboardView_backgroundDimAlpha, 0); mBackgroundDimAlphaPaint.setColor(Color.BLACK); mBackgroundDimAlphaPaint.setAlpha(backgroundDimAlpha); + mSpacebarBackground = mainKeyboardViewAttr.getDrawable( + R.styleable.MainKeyboardView_spacebarBackground); mAutoCorrectionSpacebarLedEnabled = mainKeyboardViewAttr.getBoolean( R.styleable.MainKeyboardView_autoCorrectionSpacebarLedEnabled, false); mAutoCorrectionSpacebarLedIcon = mainKeyboardViewAttr.getDrawable( R.styleable.MainKeyboardView_autoCorrectionSpacebarLedIcon); - mSpacebarTextRatio = mainKeyboardViewAttr.getFraction( - R.styleable.MainKeyboardView_spacebarTextRatio, 1, 1, 1.0f); - mSpacebarTextColor = mainKeyboardViewAttr.getColor( - R.styleable.MainKeyboardView_spacebarTextColor, 0); - mSpacebarTextShadowColor = mainKeyboardViewAttr.getColor( - R.styleable.MainKeyboardView_spacebarTextShadowColor, 0); + mLanguageOnSpacebarTextRatio = mainKeyboardViewAttr.getFraction( + R.styleable.MainKeyboardView_languageOnSpacebarTextRatio, 1, 1, 1.0f); + mLanguageOnSpacebarTextColor = mainKeyboardViewAttr.getColor( + R.styleable.MainKeyboardView_languageOnSpacebarTextColor, 0); + mLanguageOnSpacebarTextShadowColor = mainKeyboardViewAttr.getColor( + R.styleable.MainKeyboardView_languageOnSpacebarTextShadowColor, 0); mLanguageOnSpacebarFinalAlpha = mainKeyboardViewAttr.getInt( R.styleable.MainKeyboardView_languageOnSpacebarFinalAlpha, Constants.Color.ALPHA_OPAQUE); @@ -462,24 +241,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final int altCodeKeyWhileTypingFadeinAnimatorResId = mainKeyboardViewAttr.getResourceId( R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0); - final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f); - final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f); - mKeyDetector = new KeyDetector( - keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier); - mKeyTimerHandler = new KeyTimerHandler(this, mainKeyboardViewAttr); - mKeyPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset( - R.styleable.MainKeyboardView_keyPreviewOffset, 0); - mKeyPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize( - R.styleable.MainKeyboardView_keyPreviewHeight, 0); - mKeyPreviewLingerTimeout = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0); - mKeyPreviewLayoutId = mainKeyboardViewAttr.getResourceId( - R.styleable.MainKeyboardView_keyPreviewLayout, 0); - if (mKeyPreviewLayoutId == 0) { - mShowKeyPreviewPopup = false; - } + mKeyPreviewDrawParams = new KeyPreviewDrawParams(mainKeyboardViewAttr); + mKeyPreviewChoreographer = new KeyPreviewChoreographer(mKeyPreviewDrawParams); + final int moreKeysKeyboardLayoutId = mainKeyboardViewAttr.getResourceId( R.styleable.MainKeyboardView_moreKeysKeyboardLayout, 0); mConfigShowMoreKeysKeyboardAtTouchedPoint = mainKeyboardViewAttr.getBoolean( @@ -487,19 +251,18 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mGestureFloatingPreviewTextLingerTimeout = mainKeyboardViewAttr.getInt( R.styleable.MainKeyboardView_gestureFloatingPreviewTextLingerTimeout, 0); - PointerTracker.setParameters(mainKeyboardViewAttr); - mGestureFloatingPreviewText = new GestureFloatingPreviewText( - mPreviewPlacerView, mainKeyboardViewAttr); - mPreviewPlacerView.addPreview(mGestureFloatingPreviewText); + mGestureFloatingTextDrawingPreview = new GestureFloatingTextDrawingPreview( + mDrawingPreviewPlacerView, mainKeyboardViewAttr); + mDrawingPreviewPlacerView.addPreview(mGestureFloatingTextDrawingPreview); - mGestureTrailsPreview = new GestureTrailsPreview( - mPreviewPlacerView, mainKeyboardViewAttr); - mPreviewPlacerView.addPreview(mGestureTrailsPreview); + mGestureTrailsDrawingPreview = new GestureTrailsDrawingPreview( + mDrawingPreviewPlacerView, mainKeyboardViewAttr); + mDrawingPreviewPlacerView.addPreview(mGestureTrailsDrawingPreview); - mSlidingKeyInputPreview = new SlidingKeyInputPreview( - mPreviewPlacerView, mainKeyboardViewAttr); - mPreviewPlacerView.addPreview(mSlidingKeyInputPreview); + mSlidingKeyInputDrawingPreview = new SlidingKeyInputDrawingPreview( + mDrawingPreviewPlacerView, mainKeyboardViewAttr); + mDrawingPreviewPlacerView.addPreview(mSlidingKeyInputDrawingPreview); mainKeyboardViewAttr.recycle(); mMoreKeysKeyboardContainer = LayoutInflater.from(getContext()) @@ -513,14 +276,14 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER; - mLanguageOnSpacebarHorizontalMargin = - (int) getResources().getDimension(R.dimen.language_on_spacebar_horizontal_margin); + mLanguageOnSpacebarHorizontalMargin = (int)getResources().getDimension( + R.dimen.config_language_on_spacebar_horizontal_margin); } @Override public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { super.setHardwareAcceleratedDrawingEnabled(enabled); - mPreviewPlacerView.setHardwareAcceleratedDrawingEnabled(enabled); + mDrawingPreviewPlacerView.setHardwareAcceleratedDrawingEnabled(enabled); } private ObjectAnimator loadObjectAnimator(final int resId, final Object target) { @@ -536,6 +299,35 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack return animator; } + private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel, + final ObjectAnimator animatorToStart) { + if (animatorToCancel == null || animatorToStart == null) { + // TODO: Stop using null as a no-operation animator. + return; + } + float startFraction = 0.0f; + if (animatorToCancel.isStarted()) { + animatorToCancel.cancel(); + startFraction = 1.0f - animatorToCancel.getAnimatedFraction(); + } + final long startTime = (long)(animatorToStart.getDuration() * startFraction); + animatorToStart.start(); + animatorToStart.setCurrentPlayTime(startTime); + } + + // Implements {@link TimerHander.Callbacks} method. + @Override + public void startWhileTypingFadeinAnimation() { + cancelAndStartAnimators( + mAltCodeKeyWhileTypingFadeoutAnimator, mAltCodeKeyWhileTypingFadeinAnimator); + } + + @Override + public void startWhileTypingFadeoutAnimation() { + cancelAndStartAnimators( + mAltCodeKeyWhileTypingFadeinAnimator, mAltCodeKeyWhileTypingFadeoutAnimator); + } + @ExternallyReferenced public int getLanguageOnSpacebarAnimAlpha() { return mLanguageOnSpacebarAnimAlpha; @@ -573,28 +365,16 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack PointerTracker.setKeyboardActionListener(listener); } - /** - * Returns the {@link KeyboardActionListener} object. - * @return the listener attached to this keyboard - */ - @Override - public KeyboardActionListener getKeyboardActionListener() { - return mKeyboardActionListener; - } - - @Override - public KeyDetector getKeyDetector() { - return mKeyDetector; - } - - @Override - public DrawingProxy getDrawingProxy() { - return this; + // TODO: We should reconsider which coordinate system should be used to represent keyboard + // event. + public int getKeyX(final int x) { + return Constants.isValidCoordinate(x) ? mKeyDetector.getTouchX(x) : x; } - @Override - public TimerProxy getTimerProxy() { - return mKeyTimerHandler; + // TODO: We should reconsider which coordinate system should be used to represent keyboard + // event. + public int getKeyY(final int y) { + return Constants.isValidCoordinate(y) ? mKeyDetector.getTouchY(y) : y; } /** @@ -607,7 +387,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack @Override public void setKeyboard(final Keyboard keyboard) { // Remove any pending messages, except dismissing preview and key repeat. - mKeyTimerHandler.cancelLongPressTimer(); + mKeyTimerHandler.cancelLongPressTimers(); super.setKeyboard(keyboard); mKeyDetector.setKeyboard( keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection()); @@ -615,10 +395,10 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mMoreKeysKeyboardCache.clear(); mSpaceKey = keyboard.getKey(Constants.CODE_SPACE); - mSpaceIcon = (mSpaceKey != null) + mSpacebarIcon = (mSpaceKey != null) ? mSpaceKey.getIcon(keyboard.mIconsSet, Constants.Color.ALPHA_OPAQUE) : null; final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap; - mSpacebarTextSize = keyHeight * mSpacebarTextRatio; + mLanguageOnSpacebarTextSize = keyHeight * mLanguageOnSpacebarTextRatio; if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { final int orientation = getContext().getResources().getConfiguration().orientation; ResearchLogger.mainKeyboardView_setKeyboard(keyboard, orientation); @@ -637,26 +417,21 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack * @see #isKeyPreviewPopupEnabled() */ public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) { - mShowKeyPreviewPopup = previewEnabled; - mKeyPreviewLingerTimeout = delay; + mKeyPreviewDrawParams.setPopupEnabled(previewEnabled, delay); + } + + public void setKeyPreviewAnimationParams(final float showUpStartScale, final int showUpDuration, + final float dismissEndScale, final int dismissDuration) { + mKeyPreviewDrawParams.setAnimationParams( + showUpStartScale, showUpDuration, dismissEndScale, dismissDuration); } private void locatePreviewPlacerView() { - if (mPreviewPlacerView.getParent() != null) { - return; - } - final int width = getWidth(); - final int height = getHeight(); - if (width == 0 || height == 0) { - // In transient state. - return; - } getLocationInWindow(mOriginCoords); - final DisplayMetrics dm = getResources().getDisplayMetrics(); - if (CoordinateUtils.y(mOriginCoords) < dm.heightPixels / 4) { - // In transient state. - return; - } + mDrawingPreviewPlacerView.setKeyboardViewGeometry(mOriginCoords, getWidth(), getHeight()); + } + + private void installPreviewPlacerView() { final View rootView = getRootView(); if (rootView == null) { Log.w(TAG, "Cannot find root view"); @@ -665,11 +440,10 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content); // Note: It'd be very weird if we get null by android.R.id.content. if (windowContentView == null) { - Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView"); - } else { - windowContentView.addView(mPreviewPlacerView); - mPreviewPlacerView.setKeyboardViewGeometry(mOriginCoords, width, height); + Log.w(TAG, "Cannot find android.R.id.content view to add DrawingPreviewPlacerView"); + return; } + windowContentView.addView(mDrawingPreviewPlacerView); } /** @@ -678,80 +452,18 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack * @see #setKeyPreviewPopupEnabled(boolean, int) */ public boolean isKeyPreviewPopupEnabled() { - return mShowKeyPreviewPopup; - } - - private void addKeyPreview(final TextView keyPreview) { - locatePreviewPlacerView(); - mPreviewPlacerView.addView( - keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0)); - } - - private TextView getKeyPreviewText(final int pointerId) { - TextView previewText = mKeyPreviewTexts.get(pointerId); - if (previewText != null) { - return previewText; - } - final Context context = getContext(); - if (mKeyPreviewLayoutId != 0) { - previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null); - } else { - previewText = new TextView(context); - } - mKeyPreviewTexts.put(pointerId, previewText); - return previewText; + return mKeyPreviewDrawParams.isPopupEnabled(); } - private void dismissAllKeyPreviews() { - final int pointerCount = mKeyPreviewTexts.size(); - for (int id = 0; id < pointerCount; id++) { - final TextView previewText = mKeyPreviewTexts.get(id); - if (previewText != null) { - previewText.setVisibility(INVISIBLE); - } - } + // Implements {@link DrawingHandler.Callbacks} method. + @Override + public void dismissAllKeyPreviews() { + mKeyPreviewChoreographer.dismissAllKeyPreviews(); PointerTracker.setReleasedKeyGraphicsToAllKeys(); } - // Background state set - private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = { - { // STATE_MIDDLE - EMPTY_STATE_SET, - { R.attr.state_has_morekeys } - }, - { // STATE_LEFT - { R.attr.state_left_edge }, - { R.attr.state_left_edge, R.attr.state_has_morekeys } - }, - { // STATE_RIGHT - { R.attr.state_right_edge }, - { R.attr.state_right_edge, R.attr.state_has_morekeys } - } - }; - private static final int STATE_MIDDLE = 0; - private static final int STATE_LEFT = 1; - private static final int STATE_RIGHT = 2; - private static final int STATE_NORMAL = 0; - private static final int STATE_HAS_MOREKEYS = 1; - @Override - public void showKeyPreview(final PointerTracker tracker) { - final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams; - final Keyboard keyboard = getKeyboard(); - if (!mShowKeyPreviewPopup) { - previewParams.mPreviewVisibleOffset = -keyboard.mVerticalGap; - return; - } - - final TextView previewText = getKeyPreviewText(tracker.mPointerId); - // If the key preview has no parent view yet, add it to the ViewGroup which can place - // key preview absolutely in SoftInputWindow. - if (previewText.getParent() == null) { - addKeyPreview(previewText); - } - - mDrawingHandler.cancelDismissKeyPreview(tracker); - final Key key = tracker.getKey(); + public void showKeyPreview(final Key key) { // If key is invalid or IME is already closed, we must not show key preview. // Trying to show key preview while root window is closed causes // WindowManager.BadTokenException. @@ -759,97 +471,66 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack return; } - final KeyDrawParams drawParams = mKeyDrawParams; - previewText.setTextColor(drawParams.mPreviewTextColor); - final Drawable background = previewText.getBackground(); - final String label = key.getPreviewLabel(); - // What we show as preview should match what we show on a key top in onDraw(). - if (label != null) { - // TODO Should take care of temporaryShiftLabel here. - previewText.setCompoundDrawables(null, null, null, null); - previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, - key.selectPreviewTextSize(drawParams)); - previewText.setTypeface(key.selectPreviewTypeface(drawParams)); - previewText.setText(label); - } else { - previewText.setCompoundDrawables(null, null, null, - key.getPreviewIcon(keyboard.mIconsSet)); - previewText.setText(null); + final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams; + final Keyboard keyboard = getKeyboard(); + if (!previewParams.isPopupEnabled()) { + previewParams.setVisibleOffset(-keyboard.mVerticalGap); + return; } - previewText.measure( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - final int keyDrawWidth = key.getDrawWidth(); - final int previewWidth = previewText.getMeasuredWidth(); - final int previewHeight = mKeyPreviewHeight; - // The width and height of visible part of the key preview background. The content marker - // of the background 9-patch have to cover the visible part of the background. - previewParams.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft() - - previewText.getPaddingRight(); - previewParams.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop() - - previewText.getPaddingBottom(); - // The distance between the top edge of the parent key and the bottom of the visible part - // of the key preview background. - previewParams.mPreviewVisibleOffset = mKeyPreviewOffset - previewText.getPaddingBottom(); + locatePreviewPlacerView(); + final TextView previewTextView = mKeyPreviewChoreographer.getKeyPreviewTextView( + key, mDrawingPreviewPlacerView); getLocationInWindow(mOriginCoords); - // The key preview is horizontally aligned with the center of the visible part of the - // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and - // the left/right background is used if such background is specified. - final int statePosition; - int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 - + CoordinateUtils.x(mOriginCoords); - if (previewX < 0) { - previewX = 0; - statePosition = STATE_LEFT; - } else if (previewX > getWidth() - previewWidth) { - previewX = getWidth() - previewWidth; - statePosition = STATE_RIGHT; - } else { - statePosition = STATE_MIDDLE; - } - // The key preview is placed vertically above the top edge of the parent key with an - // arbitrary offset. - final int previewY = key.getY() - previewHeight + mKeyPreviewOffset - + CoordinateUtils.y(mOriginCoords); - - if (background != null) { - final int hasMoreKeys = (key.getMoreKeys() != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL; - background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]); - } - ViewLayoutUtils.placeViewAt( - previewText, previewX, previewY, previewWidth, previewHeight); - previewText.setVisibility(VISIBLE); + mKeyPreviewChoreographer.placeKeyPreview(key, previewTextView, keyboard.mIconsSet, + mKeyDrawParams, getWidth(), mOriginCoords); + mKeyPreviewChoreographer.showKeyPreview(key, previewTextView, isHardwareAccelerated()); } + // Implements {@link TimerHandler.Callbacks} method. @Override - public void dismissKeyPreview(final PointerTracker tracker) { - mDrawingHandler.dismissKeyPreview(mKeyPreviewLingerTimeout, tracker); + public void dismissKeyPreviewWithoutDelay(final Key key) { + mKeyPreviewChoreographer.dismissKeyPreview(key, false /* withAnimation */); + // To redraw key top letter. + invalidateKey(key); + } + + @Override + public void dismissKeyPreview(final Key key) { + if (!isHardwareAccelerated()) { + // TODO: Implement preference option to control key preview method and duration. + mDrawingHandler.dismissKeyPreview(mKeyPreviewDrawParams.getLingerTimeout(), key); + return; + } + mKeyPreviewChoreographer.dismissKeyPreview(key, true /* withAnimation */); } public void setSlidingKeyInputPreviewEnabled(final boolean enabled) { - mSlidingKeyInputPreview.setPreviewEnabled(enabled); + mSlidingKeyInputDrawingPreview.setPreviewEnabled(enabled); } @Override public void showSlidingKeyInputPreview(final PointerTracker tracker) { locatePreviewPlacerView(); - mSlidingKeyInputPreview.setPreviewPosition(tracker); + mSlidingKeyInputDrawingPreview.setPreviewPosition(tracker); } @Override public void dismissSlidingKeyInputPreview() { - mSlidingKeyInputPreview.dismissSlidingKeyInputPreview(); + mSlidingKeyInputDrawingPreview.dismissSlidingKeyInputPreview(); } private void setGesturePreviewMode(final boolean isGestureTrailEnabled, final boolean isGestureFloatingPreviewTextEnabled) { - mGestureFloatingPreviewText.setPreviewEnabled(isGestureFloatingPreviewTextEnabled); - mGestureTrailsPreview.setPreviewEnabled(isGestureTrailEnabled); + mGestureFloatingTextDrawingPreview.setPreviewEnabled(isGestureFloatingPreviewTextEnabled); + mGestureTrailsDrawingPreview.setPreviewEnabled(isGestureTrailEnabled); } + // Implements {@link DrawingHandler.Callbacks} method. + @Override public void showGestureFloatingPreviewText(final SuggestedWords suggestedWords) { locatePreviewPlacerView(); - mGestureFloatingPreviewText.setSuggetedWords(suggestedWords); + mGestureFloatingTextDrawingPreview.setSuggetedWords(suggestedWords); } public void dismissGestureFloatingPreviewText() { @@ -862,9 +543,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final boolean showsFloatingPreviewText) { locatePreviewPlacerView(); if (showsFloatingPreviewText) { - mGestureFloatingPreviewText.setPreviewPosition(tracker); + mGestureFloatingTextDrawingPreview.setPreviewPosition(tracker); } - mGestureTrailsPreview.setPreviewPosition(tracker); + mGestureTrailsDrawingPreview.setPreviewPosition(tracker); } // Note that this method is called from a non-UI thread. @@ -883,6 +564,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); + installPreviewPlacerView(); // Notify the ResearchLogger (development only diagnostics) that the keyboard view has // been attached. This is needed to properly show the splash screen, which requires that // the window token of the KeyboardView be non-null. @@ -894,7 +576,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - mPreviewPlacerView.removeAllViews(); + mDrawingPreviewPlacerView.removeAllViews(); // Notify the ResearchLogger (development only diagnostics) that the keyboard view has // been detached. This is needed to invalidate the reference of {@link MainKeyboardView} // to null. @@ -922,11 +604,13 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack return moreKeysKeyboardView; } + // Implements {@link TimerHandler.Callbacks} method. /** * Called when a key is long pressed. * @param tracker the pointer tracker which pressed the parent key */ - private void onLongPress(final PointerTracker tracker) { + @Override + public void onLongPress(final PointerTracker tracker) { if (isShowingMoreKeysPanel()) { return; } @@ -942,8 +626,8 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final int moreKeyCode = key.getMoreKeys()[0].mCode; tracker.onLongPressed(); listener.onPressKey(moreKeyCode, 0 /* repeatCount */, true /* isSinglePointer */); - listener.onCodeInput(moreKeyCode, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + listener.onCodeInput(moreKeyCode, Constants.NOT_A_COORDINATE, + Constants.NOT_A_COORDINATE, false /* isKeyRepeat */); listener.onReleaseKey(moreKeyCode, false /* withSliding */); return; } @@ -979,26 +663,24 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack // aligned with the bottom edge of the visible part of the key preview. // {@code mPreviewVisibleOffset} has been set appropriately in // {@link KeyboardView#showKeyPreview(PointerTracker)}. - final int pointY = key.getY() + mKeyPreviewDrawParams.mPreviewVisibleOffset; + final int pointY = key.getY() + mKeyPreviewDrawParams.getVisibleOffset(); moreKeysPanel.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener); tracker.onShowMoreKeysPanel(moreKeysPanel); + // TODO: Implement zoom in animation of more keys panel. + dismissKeyPreviewWithoutDelay(key); } - public boolean isInSlidingKeyInput() { + public boolean isInDraggingFinger() { if (isShowingMoreKeysPanel()) { return true; } - return PointerTracker.isAnyInSlidingKeyInput(); + return PointerTracker.isAnyInDraggingFinger(); } @Override public void onShowMoreKeysPanel(final MoreKeysPanel panel) { locatePreviewPlacerView(); - // TODO: Remove this check - if (panel.isShowingInParent()) { - panel.dismissMoreKeysPanel(); - } - mPreviewPlacerView.addView(panel.getContainerView()); + panel.showInParent(mDrawingPreviewPlacerView); mMoreKeysPanel = panel; dimEntireKeyboard(true /* dimmed */); } @@ -1008,15 +690,15 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } @Override - public void onCancelMoreKeysPanel(final MoreKeysPanel panel) { + public void onCancelMoreKeysPanel() { PointerTracker.dismissAllMoreKeysPanels(); } @Override - public void onDismissMoreKeysPanel(final MoreKeysPanel panel) { + public void onDismissMoreKeysPanel() { dimEntireKeyboard(false /* dimmed */); if (isShowingMoreKeysPanel()) { - mPreviewPlacerView.removeView(mMoreKeysPanel.getContainerView()); + mMoreKeysPanel.removeFromParent(); mMoreKeysPanel = null; } } @@ -1034,14 +716,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } @Override - public boolean dispatchTouchEvent(MotionEvent event) { - if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { - return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event); - } - return super.dispatchTouchEvent(event); - } - - @Override public boolean onTouchEvent(final MotionEvent me) { if (getKeyboard() == null) { return false; @@ -1049,10 +723,10 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack if (mNonDistinctMultitouchHelper != null) { if (me.getPointerCount() > 1 && mKeyTimerHandler.isInKeyRepeat()) { // Key repeating timer will be canceled if 2 or more keys are in action. - mKeyTimerHandler.cancelKeyRepeatTimer(); + mKeyTimerHandler.cancelKeyRepeatTimers(); } // Non distinct multitouch screen support - mNonDistinctMultitouchHelper.processMotionEvent(me, this); + mNonDistinctMultitouchHelper.processMotionEvent(me, mKeyDetector); return true; } return processMotionEvent(me); @@ -1069,8 +743,14 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final int index = me.getActionIndex(); final int id = me.getPointerId(index); - final PointerTracker tracker = PointerTracker.getPointerTracker(id, this); - tracker.processMotionEvent(me, this); + final PointerTracker tracker = PointerTracker.getPointerTracker(id); + // When a more keys panel is showing, we should ignore other fingers' single touch events + // other than the finger that is showing the more keys panel. + if (isShowingMoreKeysPanel() && !tracker.isShowingMoreKeysPanel() + && PointerTracker.getActivePointerTrackerCount() == 1) { + return true; + } + tracker.processMotionEvent(me, mKeyDetector); return true; } @@ -1099,8 +779,8 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack @Override public boolean dispatchHoverEvent(final MotionEvent event) { if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { - final PointerTracker tracker = PointerTracker.getPointerTracker(0, this); - return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker); + return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent( + event, mKeyDetector); } // Reflection doesn't support calling superclass methods. @@ -1121,14 +801,16 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged, - final boolean needsToDisplayLanguage, final boolean hasMultipleEnabledIMEsOrSubtypes) { - mNeedsToDisplayLanguage = needsToDisplayLanguage; + final int languageOnSpacebarFormatType, + final boolean hasMultipleEnabledIMEsOrSubtypes) { + mLanguageOnSpacebarFormatType = languageOnSpacebarFormatType; mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes; final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator; if (animator == null) { - mNeedsToDisplayLanguage = false; + mLanguageOnSpacebarFormatType = LanguageOnSpacebarHelper.FORMAT_TYPE_NONE; } else { - if (subtypeChanged && needsToDisplayLanguage) { + if (subtypeChanged + && languageOnSpacebarFormatType != LanguageOnSpacebarHelper.FORMAT_TYPE_NONE) { setLanguageOnSpacebarAnimAlpha(Constants.Color.ALPHA_OPAQUE); if (animator.isStarted()) { animator.cancel(); @@ -1169,12 +851,30 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } } + // Draw key background. + @Override + protected void onDrawKeyBackground(final Key key, final Canvas canvas, + final Drawable background) { + if (key.getCode() == Constants.CODE_SPACE) { + super.onDrawKeyBackground(key, canvas, mSpacebarBackground); + return; + } + super.onDrawKeyBackground(key, canvas, background); + } + @Override protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint, final KeyDrawParams params) { if (key.altCodeWhileTyping() && key.isEnabled()) { params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha; } + // Don't draw key top letter when key preview is showing. + if (FADE_OUT_KEY_TOP_LETTER_WHEN_KEY_IS_PRESSED + && mKeyPreviewChoreographer.isShowingKeyPreview(key)) { + // TODO: Fade out animation for the key top letter, and fade in animation for the key + // background color when the user presses the key. + return; + } final int code = key.getCode(); if (code == Constants.CODE_SPACE) { drawSpacebar(key, canvas, paint); @@ -1193,7 +893,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack private boolean fitsTextIntoWidth(final int width, final String text, final Paint paint) { final int maxTextWidth = width - mLanguageOnSpacebarHorizontalMargin * 2; paint.setTextScaleX(1.0f); - final float textWidth = TypefaceUtils.getLabelWidth(text, paint); + final float textWidth = TypefaceUtils.getStringWidth(text, paint); if (textWidth < width) { return true; } @@ -1204,29 +904,25 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack } paint.setTextScaleX(scaleX); - return TypefaceUtils.getLabelWidth(text, paint) < maxTextWidth; + return TypefaceUtils.getStringWidth(text, paint) < maxTextWidth; } // Layout language name on spacebar. private String layoutLanguageOnSpacebar(final Paint paint, final InputMethodSubtype subtype, final int width) { - // Choose appropriate language name to fit into the width. - final String fullText = SubtypeLocaleUtils.getFullDisplayName(subtype); - if (fitsTextIntoWidth(width, fullText, paint)) { - return fullText; + if (mLanguageOnSpacebarFormatType == LanguageOnSpacebarHelper.FORMAT_TYPE_FULL_LOCALE) { + final String fullText = SpacebarLanguageUtils.getFullDisplayName(subtype); + if (fitsTextIntoWidth(width, fullText, paint)) { + return fullText; + } } - final String middleText = SubtypeLocaleUtils.getMiddleDisplayName(subtype); + final String middleText = SpacebarLanguageUtils.getMiddleDisplayName(subtype); if (fitsTextIntoWidth(width, middleText, paint)) { return middleText; } - final String shortText = SubtypeLocaleUtils.getShortDisplayName(subtype); - if (fitsTextIntoWidth(width, shortText, paint)) { - return shortText; - } - return ""; } @@ -1235,20 +931,20 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack final int height = key.getHeight(); // If input language are explicitly selected. - if (mNeedsToDisplayLanguage) { + if (mLanguageOnSpacebarFormatType != LanguageOnSpacebarHelper.FORMAT_TYPE_NONE) { paint.setTextAlign(Align.CENTER); paint.setTypeface(Typeface.DEFAULT); - paint.setTextSize(mSpacebarTextSize); + paint.setTextSize(mLanguageOnSpacebarTextSize); final InputMethodSubtype subtype = getKeyboard().mId.mSubtype; final String language = layoutLanguageOnSpacebar(paint, subtype, width); // Draw language text with shadow final float descent = paint.descent(); final float textHeight = -paint.ascent() + descent; final float baseline = height / 2 + textHeight / 2; - paint.setColor(mSpacebarTextShadowColor); + paint.setColor(mLanguageOnSpacebarTextShadowColor); paint.setAlpha(mLanguageOnSpacebarAnimAlpha); canvas.drawText(language, width / 2, baseline - descent - 1, paint); - paint.setColor(mSpacebarTextColor); + paint.setColor(mLanguageOnSpacebarTextColor); paint.setAlpha(mLanguageOnSpacebarAnimAlpha); canvas.drawText(language, width / 2, baseline - descent, paint); } @@ -1260,18 +956,18 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack int x = (width - iconWidth) / 2; int y = height - iconHeight; drawIcon(canvas, mAutoCorrectionSpacebarLedIcon, x, y, iconWidth, iconHeight); - } else if (mSpaceIcon != null) { - final int iconWidth = mSpaceIcon.getIntrinsicWidth(); - final int iconHeight = mSpaceIcon.getIntrinsicHeight(); + } else if (mSpacebarIcon != null) { + final int iconWidth = mSpacebarIcon.getIntrinsicWidth(); + final int iconHeight = mSpacebarIcon.getIntrinsicHeight(); int x = (width - iconWidth) / 2; int y = height - iconHeight; - drawIcon(canvas, mSpaceIcon, x, y, iconWidth, iconHeight); + drawIcon(canvas, mSpacebarIcon, x, y, iconWidth, iconHeight); } } @Override public void deallocateMemory() { super.deallocateMemory(); - mGestureTrailsPreview.deallocateMemory(); + mDrawingPreviewPlacerView.deallocateMemory(); } } diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java index 6b76e2461..abff202b7 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java @@ -21,25 +21,29 @@ public final class MoreKeysDetector extends KeyDetector { private final int mSlideAllowanceSquareTop; public MoreKeysDetector(float slideAllowance) { - super(/* keyHysteresisDistance */0); + super(); mSlideAllowanceSquare = (int)(slideAllowance * slideAllowance); // Top slide allowance is slightly longer (sqrt(2) times) than other edges. mSlideAllowanceSquareTop = mSlideAllowanceSquare * 2; } @Override - public boolean alwaysAllowsSlidingInput() { + public boolean alwaysAllowsKeySelectionByDraggingFinger() { return true; } @Override - public Key detectHitKey(int x, int y) { + public Key detectHitKey(final int x, final int y) { + final Keyboard keyboard = getKeyboard(); + if (keyboard == null) { + return null; + } final int touchX = getTouchX(x); final int touchY = getTouchY(y); Key nearestKey = null; int nearestDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare; - for (final Key key : getKeyboard().getKeys()) { + for (final Key key : keyboard.getKeys()) { final int dist = key.squaredDistanceToEdge(touchX, touchY); if (dist < nearestDist) { nearestKey = key; diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java index 8256d4623..a72f79137 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java @@ -223,7 +223,7 @@ public final class MoreKeysKeyboard extends Keyboard { } public int getDefaultKeyCoordX() { - return mLeftKeys * mColumnWidth; + return mLeftKeys * mColumnWidth + mLeftPadding; } public int getX(final int n, final int row) { @@ -285,7 +285,7 @@ public final class MoreKeysKeyboard extends Keyboard { // {@link MoreKeysKeyboardParams#setParameters(int,int,int,int,int,int,boolean,int)}. final boolean singleMoreKeyWithPreview = parentKeyboardView.isKeyPreviewPopupEnabled() && !parentKey.noKeyPreview() && moreKeys.length == 1 - && keyPreviewDrawParams.mPreviewVisibleWidth > 0; + && keyPreviewDrawParams.getVisibleWidth() > 0; if (singleMoreKeyWithPreview) { // Use pre-computed width and height if this more keys keyboard has only one key to // mitigate visual flicker between key preview and more keys keyboard. @@ -294,11 +294,11 @@ public final class MoreKeysKeyboard extends Keyboard { // left/right/top paddings. The bottom paddings of both backgrounds don't need to // be considered because the vertical positions of both backgrounds were already // adjusted with their bottom paddings deducted. - width = keyPreviewDrawParams.mPreviewVisibleWidth; - height = keyPreviewDrawParams.mPreviewVisibleHeight + mParams.mVerticalGap; + width = keyPreviewDrawParams.getVisibleWidth(); + height = keyPreviewDrawParams.getVisibleHeight() + mParams.mVerticalGap; } else { final float padding = context.getResources().getDimension( - R.dimen.more_keys_keyboard_key_horizontal_padding) + R.dimen.config_more_keys_keyboard_key_horizontal_padding) + (parentKey.hasLabelsInMoreKeys() ? mParams.mDefaultKeyWidth * LABEL_PADDING_RATIO : 0.0f); width = getMaxKeyWidth(parentKey, mParams.mDefaultKeyWidth, padding, @@ -327,7 +327,7 @@ public final class MoreKeysKeyboard extends Keyboard { // If the label is single letter, minKeyWidth is enough to hold the label. if (label != null && StringUtils.codePointCount(label) > 1) { maxWidth = Math.max(maxWidth, - (int)(TypefaceUtils.getLabelWidth(label, paint) + padding)); + (int)(TypefaceUtils.getStringWidth(label, paint) + padding)); } } return maxWidth; @@ -343,8 +343,7 @@ public final class MoreKeysKeyboard extends Keyboard { final int row = n / params.mNumColumns; final int x = params.getX(n, row); final int y = params.getY(row); - final Key key = new Key(params, moreKeySpec, x, y, - params.mDefaultKeyWidth, params.mDefaultRowHeight, moreKeyFlags); + final Key key = moreKeySpec.buildKey(x, y, moreKeyFlags, params); params.markAsEdgeKey(key, row); params.onAddKey(key); diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java index 973128d36..65242dd76 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java @@ -21,6 +21,7 @@ import android.content.res.Resources; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; @@ -52,7 +53,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel final Resources res = context.getResources(); mKeyDetector = new MoreKeysDetector( - res.getDimension(R.dimen.more_keys_keyboard_slide_allowance)); + res.getDimension(R.dimen.config_more_keys_keyboard_slide_allowance)); } @Override @@ -81,11 +82,13 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel mListener = listener; final View container = getContainerView(); // The coordinates of panel's left-top corner in parentView's coordinate system. - final int x = pointX - getDefaultCoordX() - container.getPaddingLeft(); - final int y = pointY - container.getMeasuredHeight() + container.getPaddingBottom(); + // We need to consider background drawable paddings. + final int x = pointX - getDefaultCoordX() - container.getPaddingLeft() - getPaddingLeft(); + final int y = pointY - container.getMeasuredHeight() + container.getPaddingBottom() + + getPaddingBottom(); parentView.getLocationInWindow(mCoordinates); - // Ensure the horizontal position of the panel does not extend past the screen edges. + // Ensure the horizontal position of the panel does not extend past the parentView edges. final int maxX = parentView.getMeasuredWidth() - container.getMeasuredWidth(); final int panelX = Math.max(0, Math.min(maxX, x)) + CoordinateUtils.x(mCoordinates); final int panelY = y + CoordinateUtils.y(mCoordinates); @@ -119,7 +122,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel onMoveKeyInternal(x, y, pointerId); if (hasOldKey && mCurrentKey == null) { // If the pointer has moved too far away from any target then cancel the panel. - mController.onCancelMoreKeysPanel(this); + mController.onCancelMoreKeysPanel(); } } @@ -139,7 +142,12 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel if (code == Constants.CODE_OUTPUT_TEXT) { mListener.onTextInput(mCurrentKey.getOutputText()); } else if (code != Constants.CODE_UNSPECIFIED) { - mListener.onCodeInput(code, x, y); + if (getKeyboard().hasProximityCharsCorrection(code)) { + mListener.onCodeInput(code, x, y, false /* isKeyRepeat */); + } else { + mListener.onCodeInput(code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + false /* isKeyRepeat */); + } } } @@ -177,7 +185,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel if (!isShowingInParent()) { return; } - mController.onDismissMoreKeysPanel(this); + mController.onDismissMoreKeysPanel(); } @Override @@ -214,12 +222,26 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel return true; } - @Override - public View getContainerView() { + private View getContainerView() { return (View)getParent(); } @Override + public void showInParent(final ViewGroup parentView) { + removeFromParent(); + parentView.addView(getContainerView()); + } + + @Override + public void removeFromParent() { + final View containerView = getContainerView(); + final ViewGroup currentParent = (ViewGroup)containerView.getParent(); + if (currentParent != null) { + currentParent.removeView(containerView); + } + } + + @Override public boolean isShowingInParent() { return (getContainerView().getParent() != null); } diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java index 886c6286f..7bddd09f6 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java @@ -17,6 +17,7 @@ package com.android.inputmethod.keyboard; import android.view.View; +import android.view.ViewGroup; public interface MoreKeysPanel { public interface Controller { @@ -28,24 +29,22 @@ public interface MoreKeysPanel { /** * Remove the current {@link MoreKeysPanel} from the target view. - * @param panel the panel to be dismissed. */ - public void onDismissMoreKeysPanel(final MoreKeysPanel panel); + public void onDismissMoreKeysPanel(); /** * Instructs the parent to cancel the panel (e.g., when entering a different input mode). - * @param panel the panel to be canceled. */ - public void onCancelMoreKeysPanel(final MoreKeysPanel panel); + public void onCancelMoreKeysPanel(); } public static final Controller EMPTY_CONTROLLER = new Controller() { @Override public void onShowMoreKeysPanel(final MoreKeysPanel panel) {} @Override - public void onDismissMoreKeysPanel(final MoreKeysPanel panel) {} + public void onDismissMoreKeysPanel() {} @Override - public void onCancelMoreKeysPanel(final MoreKeysPanel panel) {} + public void onCancelMoreKeysPanel() {} }; /** @@ -119,9 +118,16 @@ public interface MoreKeysPanel { public int translateY(int y); /** - * Return the view containing the more keys panel. + * Show this {@link MoreKeysPanel} in the parent view. + * + * @param parentView the {@link ViewGroup} that hosts this {@link MoreKeysPanel}. + */ + public void showInParent(ViewGroup parentView); + + /** + * Remove this {@link MoreKeysPanel} from the parent view. */ - public View getContainerView(); + public void removeFromParent(); /** * Return whether the panel is currently being shown. diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index 52f190e77..4777166ea 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -19,16 +19,18 @@ package com.android.inputmethod.keyboard; import android.content.res.Resources; import android.content.res.TypedArray; import android.os.SystemClock; -import android.util.DisplayMetrics; import android.util.Log; import android.view.MotionEvent; -import com.android.inputmethod.accessibility.AccessibilityUtils; -import com.android.inputmethod.keyboard.internal.GestureStroke; -import com.android.inputmethod.keyboard.internal.GestureStroke.GestureStrokeParams; -import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints; -import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints.GestureStrokePreviewParams; +import com.android.inputmethod.keyboard.internal.BatchInputArbiter; +import com.android.inputmethod.keyboard.internal.BatchInputArbiter.BatchInputArbiterListener; +import com.android.inputmethod.keyboard.internal.BogusMoveEventDetector; +import com.android.inputmethod.keyboard.internal.GestureEnabler; +import com.android.inputmethod.keyboard.internal.GestureStrokeDrawingParams; +import com.android.inputmethod.keyboard.internal.GestureStrokeDrawingPoints; +import com.android.inputmethod.keyboard.internal.GestureStrokeRecognitionParams; import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; +import com.android.inputmethod.keyboard.internal.TypingTimeRecorder; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.InputPointers; import com.android.inputmethod.latin.LatinImeLogger; @@ -42,50 +44,18 @@ import com.android.inputmethod.research.ResearchLogger; import java.util.ArrayList; -public final class PointerTracker implements PointerTrackerQueue.Element { +public final class PointerTracker implements PointerTrackerQueue.Element, + BatchInputArbiterListener { private static final String TAG = PointerTracker.class.getSimpleName(); private static final boolean DEBUG_EVENT = false; private static final boolean DEBUG_MOVE_EVENT = false; private static final boolean DEBUG_LISTENER = false; private static boolean DEBUG_MODE = LatinImeLogger.sDBG || DEBUG_EVENT; - /** True if {@link PointerTracker}s should handle gesture events. */ - private static boolean sShouldHandleGesture = false; - private static boolean sMainDictionaryAvailable = false; - private static boolean sGestureHandlingEnabledByInputField = false; - private static boolean sGestureHandlingEnabledByUser = false; - - public interface KeyEventHandler { - /** - * Get KeyDetector object that is used for this PointerTracker. - * @return the KeyDetector object that is used for this PointerTracker - */ - public KeyDetector getKeyDetector(); - - /** - * Get KeyboardActionListener object that is used to register key code and so on. - * @return the KeyboardActionListner for this PointerTracke - */ - public KeyboardActionListener getKeyboardActionListener(); - - /** - * Get DrawingProxy object that is used for this PointerTracker. - * @return the DrawingProxy object that is used for this PointerTracker - */ - public DrawingProxy getDrawingProxy(); - - /** - * Get TimerProxy object that handles key repeat and long press timer event for this - * PointerTracker. - * @return the TimerProxy object that handles key repeat and long press timer event. - */ - public TimerProxy getTimerProxy(); - } - public interface DrawingProxy { public void invalidateKey(Key key); - public void showKeyPreview(PointerTracker tracker); - public void dismissKeyPreview(PointerTracker tracker); + public void showKeyPreview(Key key); + public void dismissKeyPreview(Key key); public void showSlidingKeyInputPreview(PointerTracker tracker); public void dismissSlidingKeyInputPreview(); public void showGestureTrail(PointerTracker tracker, boolean showsFloatingPreviewText); @@ -94,13 +64,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element { public interface TimerProxy { public void startTypingStateTimer(Key typedKey); public boolean isTypingState(); - public void startKeyRepeatTimer(PointerTracker tracker, int repeatCount, int delay); - public void startLongPressTimer(PointerTracker tracker, int delay); - public void cancelLongPressTimer(); + public void startKeyRepeatTimerOf(PointerTracker tracker, int repeatCount, int delay); + public void startLongPressTimerOf(PointerTracker tracker, int delay); + public void cancelLongPressTimerOf(PointerTracker tracker); + public void cancelLongPressShiftKeyTimers(); + public void cancelKeyTimersOf(PointerTracker tracker); public void startDoubleTapShiftKeyTimer(); public void cancelDoubleTapShiftKeyTimer(); public boolean isInDoubleTapShiftKeyTimeout(); - public void cancelKeyTimers(); public void startUpdateBatchInputTimer(PointerTracker tracker); public void cancelUpdateBatchInputTimer(PointerTracker tracker); public void cancelAllUpdateBatchInputTimers(); @@ -111,11 +82,15 @@ public final class PointerTracker implements PointerTrackerQueue.Element { @Override public boolean isTypingState() { return false; } @Override - public void startKeyRepeatTimer(PointerTracker tracker, int repeatCount, int delay) {} + public void startKeyRepeatTimerOf(PointerTracker tracker, int repeatCount, int delay) {} + @Override + public void startLongPressTimerOf(PointerTracker tracker, int delay) {} @Override - public void startLongPressTimer(PointerTracker tracker, int delay) {} + public void cancelLongPressTimerOf(PointerTracker tracker) {} @Override - public void cancelLongPressTimer() {} + public void cancelLongPressShiftKeyTimers() {} + @Override + public void cancelKeyTimersOf(PointerTracker tracker) {} @Override public void startDoubleTapShiftKeyTimer() {} @Override @@ -123,8 +98,6 @@ public final class PointerTracker implements PointerTrackerQueue.Element { @Override public boolean isInDoubleTapShiftKeyTimeout() { return false; } @Override - public void cancelKeyTimers() {} - @Override public void startUpdateBatchInputTimer(PointerTracker tracker) {} @Override public void cancelUpdateBatchInputTimer(PointerTracker tracker) {} @@ -134,7 +107,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } static final class PointerTrackerParams { - public final boolean mSlidingKeyInputEnabled; + public final boolean mKeySelectionByDraggingFinger; public final int mTouchNoiseThresholdTime; public final int mTouchNoiseThresholdDistance; public final int mSuppressKeyPreviewAfterBatchInputDuration; @@ -142,21 +115,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element { public final int mKeyRepeatInterval; public final int mLongPressShiftLockTimeout; - public static final PointerTrackerParams DEFAULT = new PointerTrackerParams(); - - private PointerTrackerParams() { - mSlidingKeyInputEnabled = false; - mTouchNoiseThresholdTime = 0; - mTouchNoiseThresholdDistance = 0; - mSuppressKeyPreviewAfterBatchInputDuration = 0; - mKeyRepeatStartTimeout = 0; - mKeyRepeatInterval = 0; - mLongPressShiftLockTimeout = 0; - } - public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) { - mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean( - R.styleable.MainKeyboardView_slidingKeyInputEnable, false); + mKeySelectionByDraggingFinger = mainKeyboardViewAttr.getBoolean( + R.styleable.MainKeyboardView_keySelectionByDraggingFinger, false); mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt( R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0); mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimensionPixelSize( @@ -172,149 +133,36 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } } + private static GestureEnabler sGestureEnabler = new GestureEnabler(); + // Parameters for pointer handling. private static PointerTrackerParams sParams; - private static GestureStrokeParams sGestureStrokeParams; - private static GestureStrokePreviewParams sGesturePreviewParams; + private static GestureStrokeRecognitionParams sGestureStrokeRecognitionParams; + private static GestureStrokeDrawingParams sGestureStrokeDrawingParams; private static boolean sNeedsPhantomSuddenMoveEventHack; // Move this threshold to resource. // TODO: Device specific parameter would be better for device specific hack? private static final float PHANTOM_SUDDEN_MOVE_THRESHOLD = 0.25f; // in keyWidth - // This hack is applied to certain classes of tablets. - // See {@link #needsProximateBogusDownMoveUpEventHack(Resources)}. - private static boolean sNeedsProximateBogusDownMoveUpEventHack; private static final ArrayList<PointerTracker> sTrackers = CollectionUtils.newArrayList(); private static final PointerTrackerQueue sPointerTrackerQueue = new PointerTrackerQueue(); public final int mPointerId; - private DrawingProxy mDrawingProxy; - private TimerProxy mTimerProxy; - private KeyDetector mKeyDetector; - private KeyboardActionListener mListener = KeyboardActionListener.EMPTY_LISTENER; + private static DrawingProxy sDrawingProxy; + private static TimerProxy sTimerProxy; + private static KeyboardActionListener sListener = KeyboardActionListener.EMPTY_LISTENER; + // The {@link KeyDetector} is set whenever the down event is processed. Also this is updated + // when new {@link Keyboard} is set by {@link #setKeyDetector(KeyDetector)}. + private KeyDetector mKeyDetector = new KeyDetector(); private Keyboard mKeyboard; - private int mPhantonSuddenMoveThreshold; + private int mPhantomSuddenMoveThreshold; private final BogusMoveEventDetector mBogusMoveEventDetector = new BogusMoveEventDetector(); private boolean mIsDetectingGesture = false; // per PointerTracker. private static boolean sInGesture = false; - private static long sGestureFirstDownTime; - private static TimeRecorder sTimeRecorder; - private static final InputPointers sAggregratedPointers = new InputPointers( - GestureStroke.DEFAULT_CAPACITY); - private static int sLastRecognitionPointSize = 0; // synchronized using sAggregratedPointers - private static long sLastRecognitionTime = 0; // synchronized using sAggregratedPointers - - static final class BogusMoveEventDetector { - // Move these thresholds to resource. - // These thresholds' unit is a diagonal length of a key. - private static final float BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD = 0.53f; - private static final float BOGUS_MOVE_RADIUS_THRESHOLD = 1.14f; - - private int mAccumulatedDistanceThreshold; - private int mRadiusThreshold; - - // Accumulated distance from actual and artificial down keys. - /* package */ int mAccumulatedDistanceFromDownKey; - private int mActualDownX; - private int mActualDownY; - - public void setKeyboardGeometry(final int keyWidth, final int keyHeight) { - final float keyDiagonal = (float)Math.hypot(keyWidth, keyHeight); - mAccumulatedDistanceThreshold = (int)( - keyDiagonal * BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD); - mRadiusThreshold = (int)(keyDiagonal * BOGUS_MOVE_RADIUS_THRESHOLD); - } - - public void onActualDownEvent(final int x, final int y) { - mActualDownX = x; - mActualDownY = y; - } - - public void onDownKey() { - mAccumulatedDistanceFromDownKey = 0; - } - - public void onMoveKey(final int distance) { - mAccumulatedDistanceFromDownKey += distance; - } - - public boolean hasTraveledLongDistance(final int x, final int y) { - final int dx = Math.abs(x - mActualDownX); - final int dy = Math.abs(y - mActualDownY); - // A bogus move event should be a horizontal movement. A vertical movement might be - // a sloppy typing and should be ignored. - return dx >= dy && mAccumulatedDistanceFromDownKey >= mAccumulatedDistanceThreshold; - } - - /* package */ int getDistanceFromDownEvent(final int x, final int y) { - return getDistance(x, y, mActualDownX, mActualDownY); - } - - public boolean isCloseToActualDownEvent(final int x, final int y) { - return getDistanceFromDownEvent(x, y) < mRadiusThreshold; - } - } - - static final class TimeRecorder { - private final int mSuppressKeyPreviewAfterBatchInputDuration; - private final int mStaticTimeThresholdAfterFastTyping; // msec - private long mLastTypingTime; - private long mLastLetterTypingTime; - private long mLastBatchInputTime; - - public TimeRecorder(final PointerTrackerParams pointerTrackerParams, - final GestureStrokeParams gestureStrokeParams) { - mSuppressKeyPreviewAfterBatchInputDuration = - pointerTrackerParams.mSuppressKeyPreviewAfterBatchInputDuration; - mStaticTimeThresholdAfterFastTyping = - gestureStrokeParams.mStaticTimeThresholdAfterFastTyping; - } - - public boolean isInFastTyping(final long eventTime) { - final long elapsedTimeSinceLastLetterTyping = eventTime - mLastLetterTypingTime; - return elapsedTimeSinceLastLetterTyping < mStaticTimeThresholdAfterFastTyping; - } - - private boolean wasLastInputTyping() { - return mLastTypingTime >= mLastBatchInputTime; - } - - public void onCodeInput(final int code, final long eventTime) { - // Record the letter typing time when - // 1. Letter keys are typed successively without any batch input in between. - // 2. A letter key is typed within the threshold time since the last any key typing. - // 3. A non-letter key is typed within the threshold time since the last letter key - // typing. - if (Character.isLetter(code)) { - if (wasLastInputTyping() - || eventTime - mLastTypingTime < mStaticTimeThresholdAfterFastTyping) { - mLastLetterTypingTime = eventTime; - } - } else { - if (eventTime - mLastLetterTypingTime < mStaticTimeThresholdAfterFastTyping) { - // This non-letter typing should be treated as a part of fast typing. - mLastLetterTypingTime = eventTime; - } - } - mLastTypingTime = eventTime; - } - - public void onEndBatchInput(final long eventTime) { - mLastBatchInputTime = eventTime; - } - - public long getLastLetterTypingTime() { - return mLastLetterTypingTime; - } - - public boolean needsToSuppressKeyPreviewPopup(final long eventTime) { - return !wasLastInputTyping() - && eventTime - mLastBatchInputTime < mSuppressKeyPreviewAfterBatchInputDuration; - } - } + private static TypingTimeRecorder sTypingTimeRecorder; // The position and time at which first down event occurred. private long mDownTime; @@ -341,92 +189,63 @@ public final class PointerTracker implements PointerTrackerQueue.Element { private MoreKeysPanel mMoreKeysPanel; private static final int MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT = 3; - // true if this pointer is in a sliding key input. - boolean mIsInSlidingKeyInput; - // true if this pointer is in a sliding key input from a modifier key, + // true if this pointer is in the dragging finger mode. + boolean mIsInDraggingFinger; + // true if this pointer is sliding from a modifier key and in the sliding key input mode, // so that further modifier keys should be ignored. - boolean mIsInSlidingKeyInputFromModifier; + boolean mIsInSlidingKeyInput; // if not a NOT_A_CODE, the key of this code is repeating private int mCurrentRepeatingKeyCode = Constants.NOT_A_CODE; - // true if a sliding key input is allowed. - private boolean mIsAllowedSlidingKeyInput; - - private final GestureStrokeWithPreviewPoints mGestureStrokeWithPreviewPoints; - - private static final int SMALL_TABLET_SMALLEST_WIDTH = 600; // dp - private static final int LARGE_TABLET_SMALLEST_WIDTH = 768; // dp - - private static boolean needsProximateBogusDownMoveUpEventHack(final Resources res) { - // The proximate bogus down move up event hack is needed for a device such like, - // 1) is large tablet, or 2) is small tablet and the screen density is less than hdpi. - // Though it seems odd to use screen density as criteria of the quality of the touch - // screen, the small table that has a less density screen than hdpi most likely has been - // made with the touch screen that needs the hack. - final int sw = res.getConfiguration().smallestScreenWidthDp; - final boolean isLargeTablet = (sw >= LARGE_TABLET_SMALLEST_WIDTH); - final boolean isSmallTablet = - (sw >= SMALL_TABLET_SMALLEST_WIDTH && sw < LARGE_TABLET_SMALLEST_WIDTH); - final int densityDpi = res.getDisplayMetrics().densityDpi; - final boolean hasLowDensityScreen = (densityDpi < DisplayMetrics.DENSITY_HIGH); - final boolean needsTheHack = isLargeTablet || (isSmallTablet && hasLowDensityScreen); - if (DEBUG_MODE) { - Log.d(TAG, "needsProximateBogusDownMoveUpEventHack=" + needsTheHack - + " smallestScreenWidthDp=" + sw + " densityDpi=" + densityDpi); - } - return needsTheHack; - } + // true if dragging finger is allowed. + private boolean mIsAllowedDraggingFinger; - public static void init(final Resources res) { - sNeedsPhantomSuddenMoveEventHack = Boolean.parseBoolean( - ResourceUtils.getDeviceOverrideValue( - res, R.array.phantom_sudden_move_event_device_list)); - sNeedsProximateBogusDownMoveUpEventHack = needsProximateBogusDownMoveUpEventHack(res); - sParams = PointerTrackerParams.DEFAULT; - sGestureStrokeParams = GestureStrokeParams.DEFAULT; - sGesturePreviewParams = GestureStrokePreviewParams.DEFAULT; - sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams); - } + private final BatchInputArbiter mBatchInputArbiter; + private final GestureStrokeDrawingPoints mGestureStrokeDrawingPoints; - public static void setParameters(final TypedArray mainKeyboardViewAttr) { + // TODO: Add PointerTrackerFactory singleton and move some class static methods into it. + public static void init(final TypedArray mainKeyboardViewAttr, final TimerProxy timerProxy, + final DrawingProxy drawingProxy) { sParams = new PointerTrackerParams(mainKeyboardViewAttr); - sGestureStrokeParams = new GestureStrokeParams(mainKeyboardViewAttr); - sGesturePreviewParams = new GestureStrokePreviewParams(mainKeyboardViewAttr); - sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams); - } + sGestureStrokeRecognitionParams = new GestureStrokeRecognitionParams(mainKeyboardViewAttr); + sGestureStrokeDrawingParams = new GestureStrokeDrawingParams(mainKeyboardViewAttr); + sTypingTimeRecorder = new TypingTimeRecorder( + sGestureStrokeRecognitionParams.mStaticTimeThresholdAfterFastTyping, + sParams.mSuppressKeyPreviewAfterBatchInputDuration); + + final Resources res = mainKeyboardViewAttr.getResources(); + sNeedsPhantomSuddenMoveEventHack = Boolean.parseBoolean( + ResourceUtils.getDeviceOverrideValue(res, + R.array.phantom_sudden_move_event_device_list, Boolean.FALSE.toString())); + BogusMoveEventDetector.init(res); - private static void updateGestureHandlingMode() { - sShouldHandleGesture = sMainDictionaryAvailable - && sGestureHandlingEnabledByInputField - && sGestureHandlingEnabledByUser - && !AccessibilityUtils.getInstance().isTouchExplorationEnabled(); + sTimerProxy = timerProxy; + sDrawingProxy = drawingProxy; } // Note that this method is called from a non-UI thread. public static void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { - sMainDictionaryAvailable = mainDictionaryAvailable; - updateGestureHandlingMode(); + sGestureEnabler.setMainDictionaryAvailability(mainDictionaryAvailable); } public static void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) { - sGestureHandlingEnabledByUser = gestureHandlingEnabledByUser; - updateGestureHandlingMode(); + sGestureEnabler.setGestureHandlingEnabledByUser(gestureHandlingEnabledByUser); } - public static PointerTracker getPointerTracker(final int id, final KeyEventHandler handler) { + public static PointerTracker getPointerTracker(final int id) { final ArrayList<PointerTracker> trackers = sTrackers; // Create pointer trackers until we can get 'id+1'-th tracker, if needed. for (int i = trackers.size(); i <= id; i++) { - final PointerTracker tracker = new PointerTracker(i, handler); + final PointerTracker tracker = new PointerTracker(i); trackers.add(tracker); } return trackers.get(id); } - public static boolean isAnyInSlidingKeyInput() { - return sPointerTrackerQueue.isAnyInSlidingKeyInput(); + public static boolean isAnyInDraggingFinger() { + return sPointerTrackerQueue.isAnyInDraggingFinger(); } public static void cancelAllPointerTrackers() { @@ -434,31 +253,27 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } public static void setKeyboardActionListener(final KeyboardActionListener listener) { - final int trackersSize = sTrackers.size(); - for (int i = 0; i < trackersSize; ++i) { - final PointerTracker tracker = sTrackers.get(i); - tracker.mListener = listener; - } + sListener = listener; } public static void setKeyDetector(final KeyDetector keyDetector) { + final Keyboard keyboard = keyDetector.getKeyboard(); + if (keyboard == null) { + return; + } final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); tracker.setKeyDetectorInner(keyDetector); - // Mark that keyboard layout has been changed. - tracker.mKeyboardLayoutHasBeenChanged = true; } - final Keyboard keyboard = keyDetector.getKeyboard(); - sGestureHandlingEnabledByInputField = !keyboard.mId.passwordInput(); - updateGestureHandlingMode(); + sGestureEnabler.setPasswordMode(keyboard.mId.passwordInput()); } public static void setReleasedKeyGraphicsToAllKeys() { final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); - tracker.setReleasedKeyGraphics(tracker.mCurrentKey); + tracker.setReleasedKeyGraphics(tracker.getKey()); } } @@ -466,28 +281,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element { final int trackersSize = sTrackers.size(); for (int i = 0; i < trackersSize; ++i) { final PointerTracker tracker = sTrackers.get(i); - if (tracker.isShowingMoreKeysPanel()) { - tracker.mMoreKeysPanel.dismissMoreKeysPanel(); - tracker.mMoreKeysPanel = null; - } + tracker.dismissMoreKeysPanel(); } } - private PointerTracker(final int id, final KeyEventHandler handler) { - if (handler == null) { - throw new NullPointerException(); - } + private PointerTracker(final int id) { mPointerId = id; - mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints( - id, sGestureStrokeParams, sGesturePreviewParams); - setKeyEventHandler(handler); - } - - private void setKeyEventHandler(final KeyEventHandler handler) { - setKeyDetectorInner(handler.getKeyDetector()); - mListener = handler.getKeyboardActionListener(); - mDrawingProxy = handler.getDrawingProxy(); - mTimerProxy = handler.getTimerProxy(); + mBatchInputArbiter = new BatchInputArbiter(id, sGestureStrokeRecognitionParams); + mGestureStrokeDrawingPoints = new GestureStrokeDrawingPoints(sGestureStrokeDrawingParams); } // Returns true if keyboard has been changed by this callback. @@ -500,10 +301,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (sInGesture || mIsDetectingGesture || mIsTrackingForActionDisabled) { return false; } - final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); + final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier(); if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onPress : %s%s%s%s", mPointerId, - KeyDetector.printableCode(key), + (key == null ? "none" : Constants.printableCode(key.getCode())), ignoreModifierKey ? " ignoreModifier" : "", key.isEnabled() ? "" : " disabled", repeatCount > 0 ? " repeatCount=" + repeatCount : "")); @@ -512,21 +313,21 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return false; } if (key.isEnabled()) { - mListener.onPressKey(key.getCode(), repeatCount, getActivePointerTrackerCount() == 1); + sListener.onPressKey(key.getCode(), repeatCount, getActivePointerTrackerCount() == 1); final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged; mKeyboardLayoutHasBeenChanged = false; - mTimerProxy.startTypingStateTimer(key); + sTimerProxy.startTypingStateTimer(key); return keyboardLayoutHasBeenChanged; } return false; } // Note that we need primaryCode argument because the keyboard may in shifted state and the - // primaryCode is different from {@link Key#mCode}. + // primaryCode is different from {@link Key#mKeyCode}. private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x, - final int y, final long eventTime) { - final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); - final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState(); + final int y, final long eventTime, final boolean isKeyRepeat) { + final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier(); + final boolean altersCode = key.altCodeWhileTyping() && sTimerProxy.isTypingState(); final int code = altersCode ? key.getAltCode() : primaryCode; if (DEBUG_LISTENER) { final String output = code == Constants.CODE_OUTPUT_TEXT @@ -544,24 +345,29 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state. if (key.isEnabled() || altersCode) { - sTimeRecorder.onCodeInput(code, eventTime); + sTypingTimeRecorder.onCodeInput(code, eventTime); if (code == Constants.CODE_OUTPUT_TEXT) { - mListener.onTextInput(key.getOutputText()); + sListener.onTextInput(key.getOutputText()); } else if (code != Constants.CODE_UNSPECIFIED) { - mListener.onCodeInput(code, x, y); + if (mKeyboard.hasProximityCharsCorrection(code)) { + sListener.onCodeInput(code, x, y, isKeyRepeat); + } else { + sListener.onCodeInput(code, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, isKeyRepeat); + } } } } // Note that we need primaryCode argument because the keyboard may be in shifted state and the - // primaryCode is different from {@link Key#mCode}. + // primaryCode is different from {@link Key#mKeyCode}. private void callListenerOnRelease(final Key key, final int primaryCode, final boolean withSliding) { // See the comment at {@link #callListenerOnPressAndCheckKeyboardLayoutChange(Key}}. if (sInGesture || mIsDetectingGesture || mIsTrackingForActionDisabled) { return; } - final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier(); + final boolean ignoreModifierKey = mIsInDraggingFinger && key.isModifier(); if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onRelease : %s%s%s%s", mPointerId, Constants.printableCode(primaryCode), @@ -576,7 +382,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return; } if (key.isEnabled()) { - mListener.onReleaseKey(primaryCode, withSliding); + sListener.onReleaseKey(primaryCode, withSliding); } } @@ -584,7 +390,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onFinishSlidingInput", mPointerId)); } - mListener.onFinishSlidingInput(); + sListener.onFinishSlidingInput(); } private void callListenerOnCancelInput() { @@ -594,33 +400,34 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.pointerTracker_callListenerOnCancelInput(); } - mListener.onCancelInput(); + sListener.onCancelInput(); } private void setKeyDetectorInner(final KeyDetector keyDetector) { final Keyboard keyboard = keyDetector.getKeyboard(); + if (keyboard == null) { + return; + } if (keyDetector == mKeyDetector && keyboard == mKeyboard) { return; } mKeyDetector = keyDetector; - mKeyboard = keyDetector.getKeyboard(); + mKeyboard = keyboard; + // Mark that keyboard layout has been changed. + mKeyboardLayoutHasBeenChanged = true; final int keyWidth = mKeyboard.mMostCommonKeyWidth; final int keyHeight = mKeyboard.mMostCommonKeyHeight; - mGestureStrokeWithPreviewPoints.setKeyboardGeometry(keyWidth, mKeyboard.mOccupiedHeight); - final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY); - if (newKey != mCurrentKey) { - if (mDrawingProxy != null) { - setReleasedKeyGraphics(mCurrentKey); - } - // Keep {@link #mCurrentKey} that comes from previous keyboard. - } - mPhantonSuddenMoveThreshold = (int)(keyWidth * PHANTOM_SUDDEN_MOVE_THRESHOLD); + mBatchInputArbiter.setKeyboardGeometry(keyWidth, mKeyboard.mOccupiedHeight); + // Keep {@link #mCurrentKey} that comes from previous keyboard. The key preview of + // {@link #mCurrentKey} will be dismissed by {@setReleasedKeyGraphics(Key)} via + // {@link onMoveEventInternal(int,int,long)} or {@link #onUpEventInternal(int,int,long)}. + mPhantomSuddenMoveThreshold = (int)(keyWidth * PHANTOM_SUDDEN_MOVE_THRESHOLD); mBogusMoveEventDetector.setKeyboardGeometry(keyWidth, keyHeight); } @Override - public boolean isInSlidingKeyInput() { - return mIsInSlidingKeyInput; + public boolean isInDraggingFinger() { + return mIsInDraggingFinger; } public Key getKey() { @@ -637,7 +444,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } private void setReleasedKeyGraphics(final Key key) { - mDrawingProxy.dismissKeyPreview(this); + sDrawingProxy.dismissKeyPreview(key); if (key == null) { return; } @@ -668,8 +475,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } private static boolean needsToSuppressKeyPreviewPopup(final long eventTime) { - if (!sShouldHandleGesture) return false; - return sTimeRecorder.needsToSuppressKeyPreviewPopup(eventTime); + if (!sGestureEnabler.shouldHandleGesture()) return false; + return sTypingTimeRecorder.needsToSuppressKeyPreviewPopup(eventTime); } private void setPressedKeyGraphics(final Key key, final long eventTime) { @@ -678,14 +485,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } // Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state. - final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState(); + final boolean altersCode = key.altCodeWhileTyping() && sTimerProxy.isTypingState(); final boolean needsToUpdateGraphics = key.isEnabled() || altersCode; if (!needsToUpdateGraphics) { return; } if (!key.noKeyPreview() && !sInGesture && !needsToSuppressKeyPreviewPopup(eventTime)) { - mDrawingProxy.showKeyPreview(this); + sDrawingProxy.showKeyPreview(key); } updatePressKeyGraphics(key); @@ -697,7 +504,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } } - if (key.altCodeWhileTyping() && mTimerProxy.isTypingState()) { + if (altersCode) { final int altCode = key.getAltCode(); final Key altKey = mKeyboard.getKey(altCode); if (altKey != null) { @@ -711,18 +518,18 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } } - private void updateReleaseKeyGraphics(final Key key) { + private static void updateReleaseKeyGraphics(final Key key) { key.onReleased(); - mDrawingProxy.invalidateKey(key); + sDrawingProxy.invalidateKey(key); } - private void updatePressKeyGraphics(final Key key) { + private static void updatePressKeyGraphics(final Key key) { key.onPressed(); - mDrawingProxy.invalidateKey(key); + sDrawingProxy.invalidateKey(key); } - public GestureStrokeWithPreviewPoints getGestureStrokeWithPreviewPoints() { - return mGestureStrokeWithPreviewPoints; + public GestureStrokeDrawingPoints getGestureStrokeDrawingPoints() { + return mGestureStrokeDrawingPoints; } public void getLastCoordinates(final int[] outCoords) { @@ -744,7 +551,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return onMoveToNewKey(onMoveKeyInternal(x, y), x, y); } - static int getDistance(final int x1, final int y1, final int x2, final int y2) { + private static int getDistance(final int x1, final int y1, final int x2, final int y2) { return (int)Math.hypot(x1 - x2, y1 - y2); } @@ -766,7 +573,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return newKey; } - private static int getActivePointerTrackerCount() { + /* package */ static int getActivePointerTrackerCount() { return sPointerTrackerQueue.size(); } @@ -774,91 +581,59 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return sPointerTrackerQueue.getOldestElement() == this; } - private void mayStartBatchInput(final Key key) { - if (sInGesture || !mGestureStrokeWithPreviewPoints.isStartOfAGesture()) { - return; - } - if (key == null || !Character.isLetter(key.getCode())) { - return; - } + // Implements {@link BatchInputArbiterListener}. + @Override + public void onStartBatchInput() { if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onStartBatchInput", mPointerId)); } - sInGesture = true; - synchronized (sAggregratedPointers) { - sAggregratedPointers.reset(); - sLastRecognitionPointSize = 0; - sLastRecognitionTime = 0; - mListener.onStartBatchInput(); - dismissAllMoreKeysPanels(); - } - mTimerProxy.cancelLongPressTimer(); - // A gesture floating preview text will be shown at the oldest pointer/finger on the screen. - mDrawingProxy.showGestureTrail( - this, isOldestTrackerInQueue() /* showsFloatingPreviewText */); - } - - public void updateBatchInputByTimer(final long eventTime) { - final int gestureTime = (int)(eventTime - sGestureFirstDownTime); - mGestureStrokeWithPreviewPoints.duplicateLastPointWith(gestureTime); - updateBatchInput(eventTime); + sListener.onStartBatchInput(); + dismissAllMoreKeysPanels(); + sTimerProxy.cancelLongPressTimerOf(this); } - private void mayUpdateBatchInput(final long eventTime, final Key key) { - if (key != null) { - updateBatchInput(eventTime); - } + private void showGestureTrail() { if (mIsTrackingForActionDisabled) { return; } // A gesture floating preview text will be shown at the oldest pointer/finger on the screen. - mDrawingProxy.showGestureTrail( + sDrawingProxy.showGestureTrail( this, isOldestTrackerInQueue() /* showsFloatingPreviewText */); } - private void updateBatchInput(final long eventTime) { - synchronized (sAggregratedPointers) { - final GestureStroke stroke = mGestureStrokeWithPreviewPoints; - stroke.appendIncrementalBatchPoints(sAggregratedPointers); - final int size = sAggregratedPointers.getPointerSize(); - if (size > sLastRecognitionPointSize - && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) { - if (DEBUG_LISTENER) { - Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d", mPointerId, - size)); - } - mTimerProxy.startUpdateBatchInputTimer(this); - mListener.onUpdateBatchInput(sAggregratedPointers); - // The listener may change the size of the pointers (when auto-committing - // for example), so we need to get the size from the pointers again. - sLastRecognitionPointSize = sAggregratedPointers.getPointerSize(); - sLastRecognitionTime = eventTime; - } - } + public void updateBatchInputByTimer(final long syntheticMoveEventTime) { + mBatchInputArbiter.updateBatchInputByTimer(syntheticMoveEventTime, this); } - private void mayEndBatchInput(final long eventTime) { - synchronized (sAggregratedPointers) { - mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers); - if (getActivePointerTrackerCount() == 1) { - sInGesture = false; - sTimeRecorder.onEndBatchInput(eventTime); - mTimerProxy.cancelAllUpdateBatchInputTimers(); - if (!mIsTrackingForActionDisabled) { - if (DEBUG_LISTENER) { - Log.d(TAG, String.format("[%d] onEndBatchInput : batchPoints=%d", - mPointerId, sAggregratedPointers.getPointerSize())); - } - mListener.onEndBatchInput(sAggregratedPointers); - } - } + // Implements {@link BatchInputArbiterListener}. + @Override + public void onUpdateBatchInput(final InputPointers aggregatedPointers, final long eventTime) { + if (DEBUG_LISTENER) { + Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d", mPointerId, + aggregatedPointers.getPointerSize())); } + sListener.onUpdateBatchInput(aggregatedPointers); + } + + // Implements {@link BatchInputArbiterListener}. + @Override + public void onStartUpdateBatchInputTimer() { + sTimerProxy.startUpdateBatchInputTimer(this); + } + + // Implements {@link BatchInputArbiterListener}. + @Override + public void onEndBatchInput(final InputPointers aggregatedPointers, final long eventTime) { + sTypingTimeRecorder.onEndBatchInput(eventTime); + sTimerProxy.cancelAllUpdateBatchInputTimers(); if (mIsTrackingForActionDisabled) { return; } - // A gesture floating preview text will be shown at the oldest pointer/finger on the screen. - mDrawingProxy.showGestureTrail( - this, isOldestTrackerInQueue() /* showsFloatingPreviewText */); + if (DEBUG_LISTENER) { + Log.d(TAG, String.format("[%d] onEndBatchInput : batchPoints=%d", + mPointerId, aggregatedPointers.getPointerSize())); + } + sListener.onEndBatchInput(aggregatedPointers); } private void cancelBatchInput() { @@ -871,19 +646,26 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (DEBUG_LISTENER) { Log.d(TAG, String.format("[%d] onCancelBatchInput", mPointerId)); } - mListener.onCancelBatchInput(); + sListener.onCancelBatchInput(); } - public void processMotionEvent(final MotionEvent me, final KeyEventHandler handler) { + public void processMotionEvent(final MotionEvent me, final KeyDetector keyDetector) { final int action = me.getActionMasked(); final long eventTime = me.getEventTime(); if (action == MotionEvent.ACTION_MOVE) { + // When this pointer is the only active pointer and is showing a more keys panel, + // we should ignore other pointers' motion event. + final boolean shouldIgnoreOtherPointers = + isShowingMoreKeysPanel() && getActivePointerTrackerCount() == 1; final int pointerCount = me.getPointerCount(); for (int index = 0; index < pointerCount; index++) { final int id = me.getPointerId(index); - final PointerTracker tracker = getPointerTracker(id, handler); + if (shouldIgnoreOtherPointers && id != mPointerId) { + continue; + } final int x = (int)me.getX(index); final int y = (int)me.getY(index); + final PointerTracker tracker = getPointerTracker(id); tracker.onMoveEvent(x, y, eventTime, me); } return; @@ -894,7 +676,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: - onDownEvent(x, y, eventTime, handler); + onDownEvent(x, y, eventTime, keyDetector); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: @@ -907,11 +689,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } private void onDownEvent(final int x, final int y, final long eventTime, - final KeyEventHandler handler) { + final KeyDetector keyDetector) { if (DEBUG_EVENT) { printTouchEvent("onDownEvent:", x, y, eventTime); } - setKeyEventHandler(handler); + setKeyDetectorInner(keyDetector); // Naive up-to-down noise filter. final long deltaT = eventTime - mUpTime; if (deltaT < sParams.mTouchNoiseThresholdTime) { @@ -938,7 +720,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } sPointerTrackerQueue.add(this); onDownEventInternal(x, y, eventTime); - if (!sShouldHandleGesture) { + if (!sGestureEnabler.shouldHandleGesture()) { return; } // A gesture should start only from a non-modifier key. Note that the gesture detection is @@ -946,28 +728,36 @@ public final class PointerTracker implements PointerTrackerQueue.Element { mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard() && key != null && !key.isModifier(); if (mIsDetectingGesture) { - if (getActivePointerTrackerCount() == 1) { - sGestureFirstDownTime = eventTime; - } - mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime, - sTimeRecorder.getLastLetterTypingTime()); + mBatchInputArbiter.addDownEventPoint(x, y, eventTime, + sTypingTimeRecorder.getLastLetterTypingTime(), getActivePointerTrackerCount()); + mGestureStrokeDrawingPoints.onDownEvent( + x, y, mBatchInputArbiter.getElapsedTimeSinceFirstDown(eventTime)); } } - private boolean isShowingMoreKeysPanel() { + /* package */ boolean isShowingMoreKeysPanel() { return (mMoreKeysPanel != null); } + private void dismissMoreKeysPanel() { + if (isShowingMoreKeysPanel()) { + mMoreKeysPanel.dismissMoreKeysPanel(); + mMoreKeysPanel = null; + } + } + private void onDownEventInternal(final int x, final int y, final long eventTime) { Key key = onDownKey(x, y, eventTime); - // Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding - // from modifier key, or 3) this pointer's KeyDetector always allows sliding input. - mIsAllowedSlidingKeyInput = sParams.mSlidingKeyInputEnabled + // Key selection by dragging finger is allowed when 1) key selection by dragging finger is + // enabled by configuration, 2) this pointer starts dragging from modifier key, or 3) this + // pointer's KeyDetector always allows key selection by dragging finger, such as + // {@link MoreKeysKeyboard}. + mIsAllowedDraggingFinger = sParams.mKeySelectionByDraggingFinger || (key != null && key.isModifier()) - || mKeyDetector.alwaysAllowsSlidingInput(); + || mKeyDetector.alwaysAllowsKeySelectionByDraggingFinger(); mKeyboardLayoutHasBeenChanged = false; mIsTrackingForActionDisabled = false; - resetSlidingKeyInput(); + resetKeySelectionByDraggingFinger(); if (key != null) { // This onPress call may have changed keyboard layout. Those cases are detected at // {@link #setKeyboard}. In those cases, we should update key according to the new @@ -982,43 +772,47 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } } - private void startSlidingKeyInput(final Key key) { - if (!mIsInSlidingKeyInput) { - mIsInSlidingKeyInputFromModifier = key.isModifier(); + private void startKeySelectionByDraggingFinger(final Key key) { + if (!mIsInDraggingFinger) { + mIsInSlidingKeyInput = key.isModifier(); } - mIsInSlidingKeyInput = true; + mIsInDraggingFinger = true; } - private void resetSlidingKeyInput() { + private void resetKeySelectionByDraggingFinger() { + mIsInDraggingFinger = false; mIsInSlidingKeyInput = false; - mIsInSlidingKeyInputFromModifier = false; - mDrawingProxy.dismissSlidingKeyInputPreview(); + sDrawingProxy.dismissSlidingKeyInputPreview(); } private void onGestureMoveEvent(final int x, final int y, final long eventTime, final boolean isMajorEvent, final Key key) { - final int gestureTime = (int)(eventTime - sGestureFirstDownTime); - if (mIsDetectingGesture) { - final int beforeLength = mGestureStrokeWithPreviewPoints.getLength(); - final boolean onValidArea = mGestureStrokeWithPreviewPoints.addPointOnKeyboard( - x, y, gestureTime, isMajorEvent); - if (mGestureStrokeWithPreviewPoints.getLength() > beforeLength) { - mTimerProxy.startUpdateBatchInputTimer(this); - } - // If the move event goes out from valid batch input area, cancel batch input. - if (!onValidArea) { - cancelBatchInput(); - return; - } - // If the MoreKeysPanel is showing then do not attempt to enter gesture mode. However, - // the gestured touch points are still being recorded in case the panel is dismissed. - if (isShowingMoreKeysPanel()) { - return; - } - mayStartBatchInput(key); - if (sInGesture) { - mayUpdateBatchInput(eventTime, key); + if (!mIsDetectingGesture) { + return; + } + final boolean onValidArea = mBatchInputArbiter.addMoveEventPoint( + x, y, eventTime, isMajorEvent, this); + // If the move event goes out from valid batch input area, cancel batch input. + if (!onValidArea) { + cancelBatchInput(); + return; + } + mGestureStrokeDrawingPoints.onMoveEvent( + x, y, mBatchInputArbiter.getElapsedTimeSinceFirstDown(eventTime)); + // If the MoreKeysPanel is showing then do not attempt to enter gesture mode. However, + // the gestured touch points are still being recorded in case the panel is dismissed. + if (isShowingMoreKeysPanel()) { + return; + } + if (!sInGesture && key != null && Character.isLetter(key.getCode()) + && mBatchInputArbiter.mayStartBatchInput(this)) { + sInGesture = true; + } + if (sInGesture) { + if (key != null) { + mBatchInputArbiter.updateBatchInput(eventTime, this); } + showGestureTrail(); } } @@ -1030,7 +824,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return; } - if (sShouldHandleGesture && me != null) { + if (sGestureEnabler.shouldHandleGesture() && me != null) { // Add historical points to gesture path. final int pointerIndex = me.findPointerIndex(mPointerId); final int historicalSize = me.getHistorySize(); @@ -1048,15 +842,15 @@ public final class PointerTracker implements PointerTrackerQueue.Element { final int translatedY = mMoreKeysPanel.translateY(y); mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId, eventTime); onMoveKey(x, y); - if (mIsInSlidingKeyInputFromModifier) { - mDrawingProxy.showSlidingKeyInputPreview(this); + if (mIsInSlidingKeyInput) { + sDrawingProxy.showSlidingKeyInputPreview(this); } return; } onMoveEventInternal(x, y, eventTime); } - private void processSlidingKeyInput(final Key newKey, final int x, final int y, + private void processDraggingFingerInToNewKey(final Key newKey, final int x, final int y, final long eventTime) { // This onPress call may have changed keyboard layout. Those cases are detected // at {@link #setKeyboard}. In those cases, we should update key according @@ -1110,35 +904,35 @@ public final class PointerTracker implements PointerTrackerQueue.Element { onDownEventInternal(x, y, eventTime); } - private void processSildeOutFromOldKey(final Key oldKey) { + private void processDraggingFingerOutFromOldKey(final Key oldKey) { setReleasedKeyGraphics(oldKey); callListenerOnRelease(oldKey, oldKey.getCode(), true /* withSliding */); - startSlidingKeyInput(oldKey); - mTimerProxy.cancelKeyTimers(); + startKeySelectionByDraggingFinger(oldKey); + sTimerProxy.cancelKeyTimersOf(this); } - private void slideFromOldKeyToNewKey(final Key key, final int x, final int y, + private void dragFingerFromOldKeyToNewKey(final Key key, final int x, final int y, final long eventTime, final Key oldKey, final int lastX, final int lastY) { // The pointer has been slid in to the new key from the previous key, we must call // onRelease() first to notify that the previous key has been released, then call // onPress() to notify that the new key is being pressed. - processSildeOutFromOldKey(oldKey); + processDraggingFingerOutFromOldKey(oldKey); startRepeatKey(key); - if (mIsAllowedSlidingKeyInput) { - processSlidingKeyInput(key, x, y, eventTime); + if (mIsAllowedDraggingFinger) { + processDraggingFingerInToNewKey(key, x, y, eventTime); } // HACK: On some devices, quick successive touches may be reported as a sudden move by // touch panel firmware. This hack detects such cases and translates the move event to // successive up and down events. // TODO: Should find a way to balance gesture detection and this hack. else if (sNeedsPhantomSuddenMoveEventHack - && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) { + && getDistance(x, y, lastX, lastY) >= mPhantomSuddenMoveThreshold) { processPhantomSuddenMoveHack(key, x, y, eventTime, oldKey, lastX, lastY); } // HACK: On some devices, quick successive proximate touches may be reported as a bogus // down-move-up event by touch panel firmware. This hack detects such cases and breaks // these events into separate up and down events. - else if (sNeedsProximateBogusDownMoveUpEventHack && sTimeRecorder.isInFastTyping(eventTime) + else if (sTypingTimeRecorder.isInFastTyping(eventTime) && mBogusMoveEventDetector.isCloseToActualDownEvent(x, y)) { processProximateBogusDownMoveUpEventHack(key, x, y, eventTime, oldKey, lastX, lastY); } @@ -1163,11 +957,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } } - private void slideOutFromOldKey(final Key oldKey, final int x, final int y) { + private void dragFingerOutFromOldKey(final Key oldKey, final int x, final int y) { // The pointer has been slid out from the previous key, we must call onRelease() to // notify that the previous key has been released. - processSildeOutFromOldKey(oldKey); - if (mIsAllowedSlidingKeyInput) { + processDraggingFingerOutFromOldKey(oldKey); + if (mIsAllowedDraggingFinger) { onMoveToNewKey(null, x, y); } else { if (!mIsDetectingGesture) { @@ -1182,7 +976,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { final Key oldKey = mCurrentKey; final Key newKey = onMoveKey(x, y); - if (sShouldHandleGesture) { + if (sGestureEnabler.shouldHandleGesture()) { // Register move event on gesture tracker. onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, newKey); if (sInGesture) { @@ -1194,19 +988,19 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (newKey != null) { if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) { - slideFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey, lastX, lastY); + dragFingerFromOldKeyToNewKey(newKey, x, y, eventTime, oldKey, lastX, lastY); } else if (oldKey == null) { // The pointer has been slid in to the new key, but the finger was not on any keys. // In this case, we must call onPress() to notify that the new key is being pressed. - processSlidingKeyInput(newKey, x, y, eventTime); + processDraggingFingerInToNewKey(newKey, x, y, eventTime); } } else { // newKey == null if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, newKey)) { - slideOutFromOldKey(oldKey, x, y); + dragFingerOutFromOldKey(oldKey, x, y); } } - if (mIsInSlidingKeyInputFromModifier) { - mDrawingProxy.showSlidingKeyInputPreview(this); + if (mIsInSlidingKeyInput) { + sDrawingProxy.showSlidingKeyInputPreview(this); } } @@ -1215,7 +1009,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { printTouchEvent("onUpEvent :", x, y, eventTime); } - mTimerProxy.cancelUpdateBatchInputTimer(this); + sTimerProxy.cancelUpdateBatchInputTimer(this); if (!sInGesture) { if (mCurrentKey != null && mCurrentKey.isModifier()) { // Before processing an up event of modifier key, all pointers already being @@ -1237,18 +1031,15 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (DEBUG_EVENT) { printTouchEvent("onPhntEvent:", mLastX, mLastY, eventTime); } - if (isShowingMoreKeysPanel()) { - return; - } onUpEventInternal(mLastX, mLastY, eventTime); cancelTrackingForAction(); } private void onUpEventInternal(final int x, final int y, final long eventTime) { - mTimerProxy.cancelKeyTimers(); + sTimerProxy.cancelKeyTimersOf(this); + final boolean isInDraggingFinger = mIsInDraggingFinger; final boolean isInSlidingKeyInput = mIsInSlidingKeyInput; - final boolean isInSlidingKeyInputFromModifier = mIsInSlidingKeyInputFromModifier; - resetSlidingKeyInput(); + resetKeySelectionByDraggingFinger(); mIsDetectingGesture = false; final Key currentKey = mCurrentKey; mCurrentKey = null; @@ -1272,7 +1063,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (currentKey != null) { callListenerOnRelease(currentKey, currentKey.getCode(), true /* withSliding */); } - mayEndBatchInput(eventTime); + if (mBatchInputArbiter.mayEndBatchInput( + eventTime, getActivePointerTrackerCount(), this)) { + sInGesture = false; + } + showGestureTrail(); return; } @@ -1280,11 +1075,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element { return; } if (currentKey != null && currentKey.isRepeatable() - && (currentKey.getCode() == currentRepeatingKeyCode) && !isInSlidingKeyInput) { + && (currentKey.getCode() == currentRepeatingKeyCode) && !isInDraggingFinger) { return; } detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime); - if (isInSlidingKeyInputFromModifier) { + if (isInSlidingKeyInput) { callListenerOnFinishSlidingInput(); } } @@ -1306,7 +1101,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } public void onLongPressed() { - resetSlidingKeyInput(); + resetKeySelectionByDraggingFinger(); cancelTrackingForAction(); setReleasedKeyGraphics(mCurrentKey); sPointerTrackerQueue.remove(this); @@ -1324,9 +1119,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } private void onCancelEventInternal() { - mTimerProxy.cancelKeyTimers(); + sTimerProxy.cancelKeyTimersOf(this); setReleasedKeyGraphics(mCurrentKey); - resetSlidingKeyInput(); + resetKeySelectionByDraggingFinger(); if (isShowingMoreKeysPanel()) { mMoreKeysPanel.dismissMoreKeysPanel(); mMoreKeysPanel = null; @@ -1335,9 +1130,6 @@ public final class PointerTracker implements PointerTrackerQueue.Element { private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime, final Key newKey) { - if (mKeyDetector == null) { - throw new NullPointerException("keyboard and/or key detector not set"); - } final Key curKey = mCurrentKey; if (newKey == curKey) { return false; @@ -1347,7 +1139,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } // Here curKey points to the different key from newKey. final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared( - mIsInSlidingKeyInputFromModifier); + mIsInSlidingKeyInput); final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToEdge(x, y); if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) { if (DEBUG_MODE) { @@ -1358,14 +1150,13 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } return true; } - if (sNeedsProximateBogusDownMoveUpEventHack && !mIsAllowedSlidingKeyInput - && sTimeRecorder.isInFastTyping(eventTime) + if (!mIsAllowedDraggingFinger && sTypingTimeRecorder.isInFastTyping(eventTime) && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) { if (DEBUG_MODE) { final float keyDiagonal = (float)Math.hypot( mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight); final float lengthFromDownRatio = - mBogusMoveEventDetector.mAccumulatedDistanceFromDownKey / keyDiagonal; + mBogusMoveEventDetector.getAccumulatedDistanceFromDownKey() / keyDiagonal; Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:" + " %.2f key diagonal from virtual down point", mPointerId, lengthFromDownRatio)); @@ -1376,30 +1167,34 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } private void startLongPressTimer(final Key key) { + // Note that we need to cancel all active long press shift key timers if any whenever we + // start a new long press timer for both non-shift and shift keys. + sTimerProxy.cancelLongPressShiftKeyTimers(); if (sInGesture) return; if (key == null) return; if (!key.isLongPressEnabled()) return; // Caveat: Please note that isLongPressEnabled() can be true even if the current key - // doesn't have its more keys. (e.g. spacebar, globe key) + // doesn't have its more keys. (e.g. spacebar, globe key) If we are in the dragging finger + // mode, we will disable long press timer of such key. // We always need to start the long press timer if the key has its more keys regardless of - // whether or not we are in the sliding input mode. - if (mIsInSlidingKeyInput && key.getMoreKeys() == null) return; - final int delay; - switch (key.getCode()) { - case Constants.CODE_SHIFT: - delay = sParams.mLongPressShiftLockTimeout; - break; - default: - final int longpressTimeout = Settings.getInstance().getCurrent().mKeyLongpressTimeout; - if (mIsInSlidingKeyInputFromModifier) { - // We use longer timeout for sliding finger input started from the modifier key. - delay = longpressTimeout * MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT; - } else { - delay = longpressTimeout; - } - break; + // whether or not we are in the dragging finger mode. + if (mIsInDraggingFinger && key.getMoreKeys() == null) return; + + final int delay = getLongPressTimeout(key.getCode()); + if (delay <= 0) return; + sTimerProxy.startLongPressTimerOf(this, delay); + } + + private int getLongPressTimeout(final int code) { + if (code == Constants.CODE_SHIFT) { + return sParams.mLongPressShiftLockTimeout; + } + final int longpressTimeout = Settings.getInstance().getCurrent().mKeyLongpressTimeout; + if (mIsInSlidingKeyInput) { + // We use longer timeout for sliding finger input started from the modifier key. + return longpressTimeout * MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT; } - mTimerProxy.startLongPressTimer(this, delay); + return longpressTimeout; } private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) { @@ -1409,7 +1204,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element { } final int code = key.getCode(); - callListenerOnCodeInput(key, code, x, y, eventTime); + callListenerOnCodeInput(key, code, x, y, eventTime, false /* isKeyRepeat */); callListenerOnRelease(key, code, false /* withSliding */); } @@ -1417,10 +1212,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element { if (sInGesture) return; if (key == null) return; if (!key.isRepeatable()) return; - // Don't start key repeat when we are in sliding input mode. - if (mIsInSlidingKeyInput) return; + // Don't start key repeat when we are in the dragging finger mode. + if (mIsInDraggingFinger) return; final int startRepeatCount = 1; - mTimerProxy.startKeyRepeatTimer(this, startRepeatCount, sParams.mKeyRepeatStartTimeout); + startKeyRepeatTimer(startRepeatCount); } public void onKeyRepeat(final int code, final int repeatCount) { @@ -1432,15 +1227,22 @@ public final class PointerTracker implements PointerTrackerQueue.Element { mCurrentRepeatingKeyCode = code; mIsDetectingGesture = false; final int nextRepeatCount = repeatCount + 1; - mTimerProxy.startKeyRepeatTimer(this, nextRepeatCount, sParams.mKeyRepeatInterval); + startKeyRepeatTimer(nextRepeatCount); callListenerOnPressAndCheckKeyboardLayoutChange(key, repeatCount); - callListenerOnCodeInput(key, code, mKeyX, mKeyY, SystemClock.uptimeMillis()); + callListenerOnCodeInput(key, code, mKeyX, mKeyY, SystemClock.uptimeMillis(), + true /* isKeyRepeat */); + } + + private void startKeyRepeatTimer(final int repeatCount) { + final int delay = + (repeatCount == 1) ? sParams.mKeyRepeatStartTimeout : sParams.mKeyRepeatInterval; + sTimerProxy.startKeyRepeatTimerOf(this, repeatCount, delay); } private void printTouchEvent(final String title, final int x, final int y, final long eventTime) { final Key key = mKeyDetector.detectHitKey(x, y); - final String code = KeyDetector.printableCode(key); + final String code = (key == null ? "none" : Constants.printableCode(key.getCode())); Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId, (mIsTrackingForActionDisabled ? "-" : " "), title, x, y, eventTime, code)); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/AbstractDrawingPreview.java b/java/src/com/android/inputmethod/keyboard/internal/AbstractDrawingPreview.java index b814fc162..3a72aed0d 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/AbstractDrawingPreview.java +++ b/java/src/com/android/inputmethod/keyboard/internal/AbstractDrawingPreview.java @@ -22,36 +22,47 @@ import android.view.View; import com.android.inputmethod.keyboard.PointerTracker; /** - * Abstract base class for previews that are drawn on PreviewPlacerView, e.g., - * GestureFloatingPrevewText, GestureTrail, and SlidingKeyInputPreview. + * Abstract base class for previews that are drawn on DrawingPreviewPlacerView, e.g., + * GestureFloatingTextDrawingPreview, GestureTrailsDrawingPreview, and + * SlidingKeyInputDrawingPreview. */ public abstract class AbstractDrawingPreview { private final View mDrawingView; private boolean mPreviewEnabled; + private boolean mHasValidGeometry; protected AbstractDrawingPreview(final View drawingView) { mDrawingView = drawingView; } - public final View getDrawingView() { + protected final View getDrawingView() { return mDrawingView; } - public final void setPreviewEnabled(final boolean enabled) { - mPreviewEnabled = enabled; + protected final boolean isPreviewEnabled() { + return mPreviewEnabled && mHasValidGeometry; } - public boolean isPreviewEnabled() { - return mPreviewEnabled; + public final void setPreviewEnabled(final boolean enabled) { + mPreviewEnabled = enabled; } - public void setKeyboardGeometry(final int[] originCoords, final int width, final int height) { - // Default implementation is empty. + /** + * Set {@link MainKeyboardView} geometry and position in the {@link SoftInputWindow}. + * The class that is overriding this method must call this super implementation. + * + * @param originCoords the top-left coordinates of the {@link MainKeyboardView} in + * {@link SoftInputWindow} coordinate-system. This is unused but has a point in an + * extended class, such as {@link GestureTrailsDrawingPreview}. + * @param width the width of {@link MainKeyboardView}. + * @param height the height of {@link MainKeyboardView}. + */ + public void setKeyboardViewGeometry(final int[] originCoords, final int width, + final int height) { + mHasValidGeometry = (width > 0 && height > 0); } - public void onDetachFromWindow() { - // Default implementation is empty. - } + public abstract void onDeallocateMemory(); /** * Draws the preview diff --git a/java/src/com/android/inputmethod/keyboard/internal/BatchInputArbiter.java b/java/src/com/android/inputmethod/keyboard/internal/BatchInputArbiter.java new file mode 100644 index 000000000..cd9875955 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/BatchInputArbiter.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.InputPointers; + +/** + * This class arbitrates batch input. + * An instance of this class holds a {@link GestureStrokeRecognitionPoints}. + * And it arbitrates multiple strokes gestured by multiple fingers and aggregates those gesture + * points into one batch input. + */ +public class BatchInputArbiter { + public interface BatchInputArbiterListener { + public void onStartBatchInput(); + public void onUpdateBatchInput( + final InputPointers aggregatedPointers, final long moveEventTime); + public void onStartUpdateBatchInputTimer(); + public void onEndBatchInput(final InputPointers aggregatedPointers, final long upEventTime); + } + + // The starting time of the first stroke of a gesture input. + private static long sGestureFirstDownTime; + // The {@link InputPointers} that includes all events of a gesture input. + private static final InputPointers sAggregatedPointers = new InputPointers( + Constants.DEFAULT_GESTURE_POINTS_CAPACITY); + private static int sLastRecognitionPointSize = 0; // synchronized using sAggregatedPointers + private static long sLastRecognitionTime = 0; // synchronized using sAggregatedPointers + + private final GestureStrokeRecognitionPoints mRecognitionPoints; + + public BatchInputArbiter(final int pointerId, final GestureStrokeRecognitionParams params) { + mRecognitionPoints = new GestureStrokeRecognitionPoints(pointerId, params); + } + + public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) { + mRecognitionPoints.setKeyboardGeometry(keyWidth, keyboardHeight); + } + + /** + * Calculate elapsed time since the first gesture down. + * @param eventTime the time of this event. + * @return the elapsed time in millisecond from the first gesture down. + */ + public int getElapsedTimeSinceFirstDown(final long eventTime) { + return (int)(eventTime - sGestureFirstDownTime); + } + + /** + * Add a down event point. + * @param x the x-coordinate of this down event. + * @param y the y-coordinate of this down event. + * @param downEventTime the time of this down event. + * @param lastLetterTypingTime the last typing input time. + * @param activePointerCount the number of active pointers when this pointer down event occurs. + */ + public void addDownEventPoint(final int x, final int y, final long downEventTime, + final long lastLetterTypingTime, final int activePointerCount) { + if (activePointerCount == 1) { + sGestureFirstDownTime = downEventTime; + } + final int elapsedTimeSinceFirstDown = getElapsedTimeSinceFirstDown(downEventTime); + final int elapsedTimeSinceLastTyping = (int)(downEventTime - lastLetterTypingTime); + mRecognitionPoints.addDownEventPoint( + x, y, elapsedTimeSinceFirstDown, elapsedTimeSinceLastTyping); + } + + /** + * Add a move event point. + * @param x the x-coordinate of this move event. + * @param y the y-coordinate of this move event. + * @param moveEventTime the time of this move event. + * @param isMajorEvent false if this is a historical move event. + * @param listener {@link BatchInputArbiterListener#onStartUpdateBatchInputTimer()} of this + * <code>listener</code> may be called if enough move points have been added. + * @return true if this move event occurs on the valid gesture area. + */ + public boolean addMoveEventPoint(final int x, final int y, final long moveEventTime, + final boolean isMajorEvent, final BatchInputArbiterListener listener) { + final int beforeLength = mRecognitionPoints.getLength(); + final boolean onValidArea = mRecognitionPoints.addEventPoint( + x, y, getElapsedTimeSinceFirstDown(moveEventTime), isMajorEvent); + if (mRecognitionPoints.getLength() > beforeLength) { + listener.onStartUpdateBatchInputTimer(); + } + return onValidArea; + } + + /** + * Determine whether the batch input has started or not. + * @param listener {@link BatchInputArbiterListener#onStartBatchInput()} of this + * <code>listener</code> will be called when the batch input has started successfully. + * @return true if the batch input has started successfully. + */ + public boolean mayStartBatchInput(final BatchInputArbiterListener listener) { + if (!mRecognitionPoints.isStartOfAGesture()) { + return false; + } + synchronized (sAggregatedPointers) { + sAggregatedPointers.reset(); + sLastRecognitionPointSize = 0; + sLastRecognitionTime = 0; + listener.onStartBatchInput(); + } + return true; + } + + /** + * Add synthetic move event point. After adding the point, + * {@link #updateBatchInput(long,BatchInputArbiterListener)} will be called internally. + * @param syntheticMoveEventTime the synthetic move event time. + * @param listener the listener to be passed to + * {@link #updateBatchInput(long,BatchInputArbiterListener)}. + */ + public void updateBatchInputByTimer(final long syntheticMoveEventTime, + final BatchInputArbiterListener listener) { + mRecognitionPoints.duplicateLastPointWith( + getElapsedTimeSinceFirstDown(syntheticMoveEventTime)); + updateBatchInput(syntheticMoveEventTime, listener); + } + + /** + * Determine whether we have enough gesture points to lookup dictionary. + * @param moveEventTime the time of this move event. + * @param listener {@link BatchInputArbiterListener#onUpdateBatchInput(InputPointers,long)} of + * this <code>listener</code> will be called when enough event points we have. Also + * {@link BatchInputArbiterListener#onStartUpdateBatchInputTimer()} will be called to have + * possible future synthetic move event. + */ + public void updateBatchInput(final long moveEventTime, + final BatchInputArbiterListener listener) { + synchronized (sAggregatedPointers) { + mRecognitionPoints.appendIncrementalBatchPoints(sAggregatedPointers); + final int size = sAggregatedPointers.getPointerSize(); + if (size > sLastRecognitionPointSize && mRecognitionPoints.hasRecognitionTimePast( + moveEventTime, sLastRecognitionTime)) { + listener.onUpdateBatchInput(sAggregatedPointers, moveEventTime); + listener.onStartUpdateBatchInputTimer(); + // The listener may change the size of the pointers (when auto-committing + // for example), so we need to get the size from the pointers again. + sLastRecognitionPointSize = sAggregatedPointers.getPointerSize(); + sLastRecognitionTime = moveEventTime; + } + } + } + + /** + * Determine whether the batch input has ended successfully or continues. + * @param upEventTime the time of this up event. + * @param activePointerCount the number of active pointers when this pointer up event occurs. + * @param listener {@link BatchInputArbiterListener#onEndBatchInput(InputPointers,long)} of this + * <code>listener</code> will be called when the batch input has started successfully. + * @return true if the batch input has ended successfully. + */ + public boolean mayEndBatchInput(final long upEventTime, final int activePointerCount, + final BatchInputArbiterListener listener) { + synchronized (sAggregatedPointers) { + mRecognitionPoints.appendAllBatchPoints(sAggregatedPointers); + if (activePointerCount == 1) { + listener.onEndBatchInput(sAggregatedPointers, upEventTime); + return true; + } + } + return false; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/BogusMoveEventDetector.java b/java/src/com/android/inputmethod/keyboard/internal/BogusMoveEventDetector.java new file mode 100644 index 000000000..e0589fc97 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/BogusMoveEventDetector.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.content.res.Resources; +import android.util.DisplayMetrics; +import android.util.Log; + +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.R; + +// This hack is applied to certain classes of tablets. +public final class BogusMoveEventDetector { + private static final String TAG = BogusMoveEventDetector.class.getSimpleName(); + private static final boolean DEBUG_MODE = LatinImeLogger.sDBG; + + // Move these thresholds to resource. + // These thresholds' unit is a diagonal length of a key. + private static final float BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD = 0.53f; + private static final float BOGUS_MOVE_RADIUS_THRESHOLD = 1.14f; + + private static boolean sNeedsProximateBogusDownMoveUpEventHack; + + public static void init(final Resources res) { + // The proximate bogus down move up event hack is needed for a device such like, + // 1) is large tablet, or 2) is small tablet and the screen density is less than hdpi. + // Though it seems odd to use screen density as criteria of the quality of the touch + // screen, the small table that has a less density screen than hdpi most likely has been + // made with the touch screen that needs the hack. + final int screenMetrics = res.getInteger(R.integer.config_screen_metrics); + final boolean isLargeTablet = (screenMetrics == Constants.SCREEN_METRICS_LARGE_TABLET); + final boolean isSmallTablet = (screenMetrics == Constants.SCREEN_METRICS_SMALL_TABLET); + final int densityDpi = res.getDisplayMetrics().densityDpi; + final boolean hasLowDensityScreen = (densityDpi < DisplayMetrics.DENSITY_HIGH); + final boolean needsTheHack = isLargeTablet || (isSmallTablet && hasLowDensityScreen); + if (DEBUG_MODE) { + final int sw = res.getConfiguration().smallestScreenWidthDp; + Log.d(TAG, "needsProximateBogusDownMoveUpEventHack=" + needsTheHack + + " smallestScreenWidthDp=" + sw + " densityDpi=" + densityDpi + + " screenMetrics=" + screenMetrics); + } + sNeedsProximateBogusDownMoveUpEventHack = needsTheHack; + } + + private int mAccumulatedDistanceThreshold; + private int mRadiusThreshold; + + // Accumulated distance from actual and artificial down keys. + /* package */ int mAccumulatedDistanceFromDownKey; + private int mActualDownX; + private int mActualDownY; + + public void setKeyboardGeometry(final int keyWidth, final int keyHeight) { + final float keyDiagonal = (float)Math.hypot(keyWidth, keyHeight); + mAccumulatedDistanceThreshold = (int)( + keyDiagonal * BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD); + mRadiusThreshold = (int)(keyDiagonal * BOGUS_MOVE_RADIUS_THRESHOLD); + } + + public void onActualDownEvent(final int x, final int y) { + mActualDownX = x; + mActualDownY = y; + } + + public void onDownKey() { + mAccumulatedDistanceFromDownKey = 0; + } + + public void onMoveKey(final int distance) { + mAccumulatedDistanceFromDownKey += distance; + } + + public boolean hasTraveledLongDistance(final int x, final int y) { + if (!sNeedsProximateBogusDownMoveUpEventHack) { + return false; + } + final int dx = Math.abs(x - mActualDownX); + final int dy = Math.abs(y - mActualDownY); + // A bogus move event should be a horizontal movement. A vertical movement might be + // a sloppy typing and should be ignored. + return dx >= dy && mAccumulatedDistanceFromDownKey >= mAccumulatedDistanceThreshold; + } + + public int getAccumulatedDistanceFromDownKey() { + return mAccumulatedDistanceFromDownKey; + } + + public int getDistanceFromDownEvent(final int x, final int y) { + return getDistance(x, y, mActualDownX, mActualDownY); + } + + private static int getDistance(final int x1, final int y1, final int x2, final int y2) { + return (int)Math.hypot(x1 - x2, y1 - y2); + } + + public boolean isCloseToActualDownEvent(final int x, final int y) { + return sNeedsProximateBogusDownMoveUpEventHack + && getDistanceFromDownEvent(x, y) < mRadiusThreshold; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java index 4ccecb2f0..dce7fc57e 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java @@ -17,6 +17,7 @@ package com.android.inputmethod.keyboard.internal; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.utils.StringUtils; import android.text.TextUtils; @@ -29,15 +30,16 @@ import android.text.TextUtils; * marker. An output text may consist of multiple code points separated by comma. * The format of the codesArray element should be: * <pre> - * codePointInHex[,codePoint2InHex]*(|outputTextCodePointInHex[,outputTextCodePoint2InHex]*)? + * label1[,label2]*(|outputText1[,outputText2]*(|minSupportSdkVersion)?)? * </pre> */ // TODO: Write unit tests for this class. public final class CodesArrayParser { // Constants for parsing. - private static final char COMMA = ','; - private static final String VERTICAL_BAR_STRING = "\\|"; - private static final String COMMA_STRING = ","; + private static final char COMMA = Constants.CODE_COMMA; + private static final String COMMA_REGEX = StringUtils.newSingleCodePointString(COMMA); + private static final String VERTICAL_BAR_REGEX = // "\\|" + new String(new char[] { Constants.CODE_BACKSLASH, Constants.CODE_VERTICAL_BAR }); private static final int BASE_HEX = 16; private CodesArrayParser() { @@ -45,7 +47,7 @@ public final class CodesArrayParser { } private static String getLabelSpec(final String codesArraySpec) { - final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1); if (strs.length <= 1) { return codesArraySpec; } @@ -55,7 +57,7 @@ public final class CodesArrayParser { public static String parseLabel(final String codesArraySpec) { final String labelSpec = getLabelSpec(codesArraySpec); final StringBuilder sb = new StringBuilder(); - for (final String codeInHex : labelSpec.split(COMMA_STRING)) { + for (final String codeInHex : labelSpec.split(COMMA_REGEX)) { final int codePoint = Integer.parseInt(codeInHex, BASE_HEX); sb.appendCodePoint(codePoint); } @@ -63,17 +65,15 @@ public final class CodesArrayParser { } private static String getCodeSpec(final String codesArraySpec) { - final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1); if (strs.length <= 1) { return codesArraySpec; } return TextUtils.isEmpty(strs[1]) ? strs[0] : strs[1]; } - // codesArraySpec consists of: - // <label>|<code0>,<code1>,...|<minSupportSdkVersion> public static int getMinSupportSdkVersion(final String codesArraySpec) { - final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1); + final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1); if (strs.length <= 2) { return 0; } @@ -98,7 +98,7 @@ public final class CodesArrayParser { return null; } final StringBuilder sb = new StringBuilder(); - for (final String codeInHex : codeSpec.split(COMMA_STRING)) { + for (final String codeInHex : codeSpec.split(COMMA_REGEX)) { final int codePoint = Integer.parseInt(codeInHex, BASE_HEX); sb.appendCodePoint(codePoint); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java b/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java new file mode 100644 index 000000000..df82becae --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/DrawingHandler.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.os.Message; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.internal.DrawingHandler.Callbacks; +import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; + +// TODO: Separate this class into KeyPreviewHandler and BatchInputPreviewHandler or so. +public class DrawingHandler extends LeakGuardHandlerWrapper<Callbacks> { + public interface Callbacks { + public void dismissKeyPreviewWithoutDelay(Key key); + public void dismissAllKeyPreviews(); + public void showGestureFloatingPreviewText(SuggestedWords suggestedWords); + } + + 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) { + super(ownerInstance); + } + + @Override + public void handleMessage(final Message msg) { + final Callbacks callbacks = getOwnerInstance(); + if (callbacks == null) { + return; + } + switch (msg.what) { + case MSG_DISMISS_KEY_PREVIEW: + callbacks.dismissKeyPreviewWithoutDelay((Key)msg.obj); + break; + case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT: + callbacks.showGestureFloatingPreviewText(SuggestedWords.EMPTY); + break; + } + } + + public void dismissKeyPreview(final long delay, final Key key) { + sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, key), delay); + } + + private void cancelAllDismissKeyPreviews() { + removeMessages(MSG_DISMISS_KEY_PREVIEW); + final Callbacks callbacks = getOwnerInstance(); + if (callbacks == null) { + return; + } + callbacks.dismissAllKeyPreviews(); + } + + public void dismissGestureFloatingPreviewText(final long delay) { + sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT), delay); + } + + public void cancelAllMessages() { + cancelAllDismissKeyPreviews(); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/DrawingPreviewPlacerView.java index 4c8607da8..fdc2458d4 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java +++ b/java/src/com/android/inputmethod/keyboard/internal/DrawingPreviewPlacerView.java @@ -29,12 +29,12 @@ import com.android.inputmethod.latin.utils.CoordinateUtils; import java.util.ArrayList; -public final class PreviewPlacerView extends RelativeLayout { +public final class DrawingPreviewPlacerView extends RelativeLayout { private final int[] mKeyboardViewOrigin = CoordinateUtils.newInstance(); private final ArrayList<AbstractDrawingPreview> mPreviews = CollectionUtils.newArrayList(); - public PreviewPlacerView(final Context context, final AttributeSet attrs) { + public DrawingPreviewPlacerView(final Context context, final AttributeSet attrs) { super(context, attrs); setWillNotDraw(false); } @@ -55,20 +55,24 @@ public final class PreviewPlacerView extends RelativeLayout { CoordinateUtils.copy(mKeyboardViewOrigin, originCoords); final int count = mPreviews.size(); for (int i = 0; i < count; i++) { - mPreviews.get(i).setKeyboardGeometry(originCoords, width, height); + mPreviews.get(i).setKeyboardViewGeometry(originCoords, width, height); } } - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); + public void deallocateMemory() { final int count = mPreviews.size(); for (int i = 0; i < count; i++) { - mPreviews.get(i).onDetachFromWindow(); + mPreviews.get(i).onDeallocateMemory(); } } @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + deallocateMemory(); + } + + @Override public void onDraw(final Canvas canvas) { super.onDraw(canvas); final int originX = CoordinateUtils.x(mKeyboardViewOrigin); diff --git a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java index 3133e54be..e2fd39017 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java +++ b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java @@ -25,7 +25,7 @@ import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.StringUtils; +import com.android.inputmethod.latin.utils.JsonUtils; import java.util.ArrayDeque; import java.util.ArrayList; @@ -53,7 +53,7 @@ public class DynamicGridKeyboard extends Keyboard { private Key[] mCachedGridKeys; public DynamicGridKeyboard(final SharedPreferences prefs, final Keyboard templateKeyboard, - final int maxKeyCount, final int categoryId, final int categoryPageId) { + final int maxKeyCount, final int categoryId) { super(templateKeyboard); final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0); final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1); @@ -124,7 +124,7 @@ public class DynamicGridKeyboard extends Keyboard { final int keyY0 = getKeyY0(index); final int keyX1 = getKeyX1(index); final int keyY1 = getKeyY1(index); - gridKey.updateCorrdinates(keyX0, keyY0, keyX1, keyY1); + gridKey.updateCoordinates(keyX0, keyY0, keyX1, keyY1); index++; } } @@ -139,36 +139,48 @@ public class DynamicGridKeyboard extends Keyboard { keys.add(key.getCode()); } } - final String jsonStr = StringUtils.listToJsonStr(keys); + final String jsonStr = JsonUtils.listToJsonStr(keys); Settings.writeEmojiRecentKeys(mPrefs, jsonStr); } - private static Key getKey(final Collection<DynamicGridKeyboard> keyboards, final Object o) { + private static Key getKeyByCode(final Collection<DynamicGridKeyboard> keyboards, + final int code) { + for (final DynamicGridKeyboard keyboard : keyboards) { + final Key key = keyboard.getKey(code); + if (key != null) { + return key; + } + } + return null; + } + + private static Key getKeyByOutputText(final Collection<DynamicGridKeyboard> keyboards, + final String outputText) { for (final DynamicGridKeyboard kbd : keyboards) { - if (o instanceof Integer) { - final int code = (Integer) o; - final Key key = kbd.getKey(code); - if (key != null) { - return key; - } - } else if (o instanceof String) { - final String outputText = (String) o; - final Key key = kbd.getKeyFromOutputText(outputText); - if (key != null) { - return key; - } - } else { - Log.w(TAG, "Invalid object: " + o); + final Key key = kbd.getKeyFromOutputText(outputText); + if (key != null) { + return key; } } return null; } - public void loadRecentKeys(Collection<DynamicGridKeyboard> keyboards) { + public void loadRecentKeys(final Collection<DynamicGridKeyboard> keyboards) { final String str = Settings.readEmojiRecentKeys(mPrefs); - final List<Object> keys = StringUtils.jsonStrToList(str); + final List<Object> keys = JsonUtils.jsonStrToList(str); for (final Object o : keys) { - addKeyLast(getKey(keyboards, o)); + final Key key; + if (o instanceof Integer) { + final int code = (Integer)o; + key = getKeyByCode(keyboards, code); + } else if (o instanceof String) { + final String outputText = (String)o; + key = getKeyByOutputText(keyboards, outputText); + } else { + Log.w(TAG, "Invalid object: " + o); + continue; + } + addKeyLast(key); } } @@ -217,7 +229,7 @@ public class DynamicGridKeyboard extends Keyboard { super(originalKey); } - public void updateCorrdinates(final int x0, final int y0, final int x1, final int y1) { + public void updateCoordinates(final int x0, final int y0, final int x1, final int y1) { mCurrentX = x0; mCurrentY = y0; getHitBox().set(x0, y0, x1, y1); diff --git a/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java b/java/src/com/android/inputmethod/keyboard/internal/EmojiLayoutParams.java index 967448c28..d57ea5a94 100644 --- a/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java +++ b/java/src/com/android/inputmethod/keyboard/internal/EmojiLayoutParams.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.inputmethod.keyboard; +package com.android.inputmethod.keyboard.internal; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.ResourceUtils; @@ -37,22 +37,22 @@ public class EmojiLayoutParams { private final int mBottomPadding; private final int mTopPadding; - public EmojiLayoutParams(Resources res) { + public EmojiLayoutParams(final Resources res) { final int defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res); final int defaultKeyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res); - mKeyVerticalGap = (int) res.getFraction(R.fraction.key_bottom_gap_holo, - (int) defaultKeyboardHeight, (int) defaultKeyboardHeight); - mBottomPadding = (int) res.getFraction(R.fraction.keyboard_bottom_padding_holo, - (int) defaultKeyboardHeight, (int) defaultKeyboardHeight); - mTopPadding = (int) res.getFraction(R.fraction.keyboard_top_padding_holo, - (int) defaultKeyboardHeight, (int) defaultKeyboardHeight); - mKeyHorizontalGap = (int) (res.getFraction(R.fraction.key_horizontal_gap_holo, + mKeyVerticalGap = (int) res.getFraction(R.fraction.config_key_vertical_gap_holo, + defaultKeyboardHeight, defaultKeyboardHeight); + mBottomPadding = (int) res.getFraction(R.fraction.config_keyboard_bottom_padding_holo, + defaultKeyboardHeight, defaultKeyboardHeight); + mTopPadding = (int) res.getFraction(R.fraction.config_keyboard_top_padding_holo, + defaultKeyboardHeight, defaultKeyboardHeight); + mKeyHorizontalGap = (int) (res.getFraction(R.fraction.config_key_horizontal_gap_holo, defaultKeyboardWidth, defaultKeyboardWidth)); mEmojiCategoryPageIdViewHeight = - (int) (res.getDimension(R.dimen.emoji_category_page_id_height)); + (int) (res.getDimension(R.dimen.config_emoji_category_page_id_height)); final int baseheight = defaultKeyboardHeight - mBottomPadding - mTopPadding + mKeyVerticalGap; - mEmojiActionBarHeight = ((int) baseheight) / DEFAULT_KEYBOARD_ROWS + mEmojiActionBarHeight = baseheight / DEFAULT_KEYBOARD_ROWS - (mKeyVerticalGap - mBottomPadding) / 2; mEmojiPagerHeight = defaultKeyboardHeight - mEmojiActionBarHeight - mEmojiCategoryPageIdViewHeight; @@ -60,26 +60,30 @@ public class EmojiLayoutParams { mEmojiKeyboardHeight = mEmojiPagerHeight - mEmojiPagerBottomMargin - 1; } - public void setPagerProperties(ViewPager vp) { + public void setPagerProperties(final ViewPager vp) { final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) vp.getLayoutParams(); lp.height = mEmojiKeyboardHeight; lp.bottomMargin = mEmojiPagerBottomMargin; vp.setLayoutParams(lp); } - public void setCategoryPageIdViewProperties(LinearLayout ll) { + public void setCategoryPageIdViewProperties(final LinearLayout ll) { final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams(); lp.height = mEmojiCategoryPageIdViewHeight; ll.setLayoutParams(lp); } - public void setActionBarProperties(LinearLayout ll) { + public int getActionBarHeight() { + return mEmojiActionBarHeight - mBottomPadding; + } + + public void setActionBarProperties(final LinearLayout ll) { final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams(); - lp.height = mEmojiActionBarHeight - mBottomPadding; + lp.height = getActionBarHeight(); ll.setLayoutParams(lp); } - public void setKeyProperties(ImageView ib) { + public void setKeyProperties(final ImageView ib) { final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ib.getLayoutParams(); lp.leftMargin = mKeyHorizontalGap / 2; lp.rightMargin = mKeyHorizontalGap / 2; diff --git a/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java b/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java index 9cf68d43d..e175a051e 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/internal/EmojiPageKeyboardView.java @@ -17,101 +17,57 @@ package com.android.inputmethod.keyboard.internal; import android.content.Context; +import android.os.Handler; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; -import android.widget.ScrollView; -import android.widget.Scroller; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.KeyDetector; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardView; +import com.android.inputmethod.keyboard.PointerTracker; import com.android.inputmethod.latin.R; /** - * This is an extended {@link KeyboardView} class that hosts a vertical scroll keyboard. + * This is an extended {@link KeyboardView} class that hosts an emoji page keyboard. * Multi-touch unsupported. No {@link PointerTracker}s. No gesture support. - * TODO: Vertical scroll capability should be removed from this class because it's no longer used. */ // TODO: Implement key popup preview. -public final class ScrollKeyboardView extends KeyboardView implements - ScrollViewWithNotifier.ScrollListener, GestureDetector.OnGestureListener { - private static final boolean PAGINATION = false; +public final class EmojiPageKeyboardView extends KeyboardView implements + GestureDetector.OnGestureListener { + private static final long KEY_PRESS_DELAY_TIME = 250; // msec + private static final long KEY_RELEASE_DELAY_TIME = 30; // msec - public interface OnKeyClickListener { - public void onKeyClick(Key key); + public interface OnKeyEventListener { + public void onPressKey(Key key); + public void onReleaseKey(Key key); } - private static final OnKeyClickListener EMPTY_LISTENER = new OnKeyClickListener() { + private static final OnKeyEventListener EMPTY_LISTENER = new OnKeyEventListener() { @Override - public void onKeyClick(final Key key) {} + public void onPressKey(final Key key) {} + @Override + public void onReleaseKey(final Key key) {} }; - private OnKeyClickListener mListener = EMPTY_LISTENER; - private final KeyDetector mKeyDetector = new KeyDetector(0.0f /*keyHysteresisDistance */); + private OnKeyEventListener mListener = EMPTY_LISTENER; + private final KeyDetector mKeyDetector = new KeyDetector(); private final GestureDetector mGestureDetector; - private final Scroller mScroller; - private ScrollViewWithNotifier mScrollView; - - public ScrollKeyboardView(final Context context, final AttributeSet attrs) { + public EmojiPageKeyboardView(final Context context, final AttributeSet attrs) { this(context, attrs, R.attr.keyboardViewStyle); } - public ScrollKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) { + public EmojiPageKeyboardView(final Context context, final AttributeSet attrs, + final int defStyle) { super(context, attrs, defStyle); mGestureDetector = new GestureDetector(context, this); mGestureDetector.setIsLongpressEnabled(false /* isLongpressEnabled */); - mScroller = new Scroller(context); - } - - public void setScrollView(final ScrollViewWithNotifier scrollView) { - mScrollView = scrollView; - scrollView.setScrollListener(this); - } - - private final Runnable mScrollTask = new Runnable() { - @Override - public void run() { - final Scroller scroller = mScroller; - final ScrollView scrollView = mScrollView; - scroller.computeScrollOffset(); - scrollView.scrollTo(0, scroller.getCurrY()); - if (!scroller.isFinished()) { - scrollView.post(this); - } - } - }; - - // {@link ScrollViewWithNotified#ScrollListener} methods. - @Override - public void notifyScrollChanged(final int scrollX, final int scrollY, final int oldX, - final int oldY) { - if (PAGINATION) { - mScroller.forceFinished(true /* finished */); - mScrollView.removeCallbacks(mScrollTask); - final int currentTop = mScrollView.getScrollY(); - final int pageHeight = getKeyboard().mBaseHeight; - final int lastPageNo = currentTop / pageHeight; - final int lastPageTop = lastPageNo * pageHeight; - final int nextPageNo = lastPageNo + 1; - final int nextPageTop = Math.min(nextPageNo * pageHeight, getHeight() - pageHeight); - final int scrollTo = (currentTop - lastPageTop) < (nextPageTop - currentTop) - ? lastPageTop : nextPageTop; - final int deltaY = scrollTo - currentTop; - mScroller.startScroll(0, currentTop, 0, deltaY, 300); - mScrollView.post(mScrollTask); - } - } - - @Override - public void notifyOverScrolled(final int scrollX, final int scrollY, final boolean clampedX, - final boolean clampedY) { - releaseCurrentKey(); + mHandler = new Handler(); } - public void setOnKeyClickListener(final OnKeyClickListener listener) { + public void setOnKeyEventListener(final OnKeyEventListener listener) { mListener = listener; } @@ -139,8 +95,10 @@ public final class ScrollKeyboardView extends KeyboardView implements return true; } - // {@link GestureDetector#OnGestureListener} methods. + // {@link GestureEnabler#OnGestureListener} methods. private Key mCurrentKey; + private Runnable mPendingKeyDown; + private final Handler mHandler; private Key getKey(final MotionEvent e) { final int index = e.getActionIndex(); @@ -150,6 +108,8 @@ public final class ScrollKeyboardView extends KeyboardView implements } public void releaseCurrentKey() { + mHandler.removeCallbacks(mPendingKeyDown); + mPendingKeyDown = null; final Key currentKey = mCurrentKey; if (currentKey == null) { return; @@ -167,9 +127,17 @@ public final class ScrollKeyboardView extends KeyboardView implements if (key == null) { return false; } - // TODO: May call {@link KeyboardActionListener#onPressKey(int,int,boolean)}. - key.onPressed(); - invalidateKey(key); + // Do not trigger key-down effect right now in case this is actually a fling action. + mPendingKeyDown = new Runnable() { + @Override + public void run() { + mPendingKeyDown = null; + key.onPressed(); + invalidateKey(key); + mListener.onPressKey(key); + } + }; + mHandler.postDelayed(mPendingKeyDown, KEY_PRESS_DELAY_TIME); return false; } @@ -181,14 +149,28 @@ public final class ScrollKeyboardView extends KeyboardView implements @Override public boolean onSingleTapUp(final MotionEvent e) { final Key key = getKey(e); + final Runnable pendingKeyDown = mPendingKeyDown; + final Key currentKey = mCurrentKey; releaseCurrentKey(); if (key == null) { return false; } - // TODO: May call {@link KeyboardActionListener#onReleaseKey(int,boolean)}. - key.onReleased(); - invalidateKey(key); - mListener.onKeyClick(key); + if (key == currentKey && pendingKeyDown != null) { + pendingKeyDown.run(); + // Trigger key-release event a little later so that a user can see visual feedback. + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + key.onReleased(); + invalidateKey(key); + mListener.onReleaseKey(key); + } + }, KEY_RELEASE_DELAY_TIME); + } else { + key.onReleased(); + invalidateKey(key); + mListener.onReleaseKey(key); + } return true; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureEnabler.java b/java/src/com/android/inputmethod/keyboard/internal/GestureEnabler.java new file mode 100644 index 000000000..7d14ae924 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureEnabler.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import com.android.inputmethod.accessibility.AccessibilityUtils; + +public final class GestureEnabler { + /** True if we should handle gesture events. */ + private boolean mShouldHandleGesture; + private boolean mMainDictionaryAvailable; + private boolean mGestureHandlingEnabledByInputField; + private boolean mGestureHandlingEnabledByUser; + + private void updateGestureHandlingMode() { + mShouldHandleGesture = mMainDictionaryAvailable + && mGestureHandlingEnabledByInputField + && mGestureHandlingEnabledByUser + && !AccessibilityUtils.getInstance().isTouchExplorationEnabled(); + } + + // Note that this method is called from a non-UI thread. + public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) { + mMainDictionaryAvailable = mainDictionaryAvailable; + updateGestureHandlingMode(); + } + + public void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) { + mGestureHandlingEnabledByUser = gestureHandlingEnabledByUser; + updateGestureHandlingMode(); + } + + public void setPasswordMode(final boolean passwordMode) { + mGestureHandlingEnabledByInputField = !passwordMode; + updateGestureHandlingMode(); + } + + public boolean shouldHandleGesture() { + return mShouldHandleGesture; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java index c6dd9e100..2fa703083 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java @@ -42,7 +42,7 @@ import com.android.inputmethod.latin.utils.CoordinateUtils; * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewVerticalPadding * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewRoundRadius */ -public class GestureFloatingPreviewText extends AbstractDrawingPreview { +public class GestureFloatingTextDrawingPreview extends AbstractDrawingPreview { protected static final class GesturePreviewTextParams { public final int mGesturePreviewTextOffset; public final int mGesturePreviewTextHeight; @@ -100,11 +100,16 @@ public class GestureFloatingPreviewText extends AbstractDrawingPreview { private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; private final int[] mLastPointerCoords = CoordinateUtils.newInstance(); - public GestureFloatingPreviewText(final View drawingView, final TypedArray typedArray) { + public GestureFloatingTextDrawingPreview(final View drawingView, final TypedArray typedArray) { super(drawingView); mParams = new GesturePreviewTextParams(typedArray); } + @Override + public void onDeallocateMemory() { + // Nothing to do here. + } + public void setSuggetedWords(final SuggestedWords suggestedWords) { if (!isPreviewEnabled()) { return; diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeDrawingParams.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeDrawingParams.java new file mode 100644 index 000000000..478639d2d --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeDrawingParams.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.content.res.TypedArray; + +import com.android.inputmethod.latin.R; + +/** + * This class holds parameters to control how a gesture stroke is sampled and drawn on the screen. + * + * @attr ref R.styleable#MainKeyboardView_gestureTrailMinSamplingDistance + * @attr ref R.styleable#MainKeyboardView_gestureTrailMaxInterpolationAngularThreshold + * @attr ref R.styleable#MainKeyboardView_gestureTrailMaxInterpolationDistanceThreshold + * @attr ref R.styleable#MainKeyboardView_gestureTrailMaxInterpolationSegments + */ +public final class GestureStrokeDrawingParams { + public final double mMinSamplingDistance; // in pixel + public final double mMaxInterpolationAngularThreshold; // in radian + public final double mMaxInterpolationDistanceThreshold; // in pixel + public final int mMaxInterpolationSegments; + + private static final float DEFAULT_MIN_SAMPLING_DISTANCE = 0.0f; // dp + private static final int DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD = 15; // in degree + private static final float DEFAULT_MAX_INTERPOLATION_DISTANCE_THRESHOLD = 0.0f; // dp + private static final int DEFAULT_MAX_INTERPOLATION_SEGMENTS = 4; + + public GestureStrokeDrawingParams(final TypedArray mainKeyboardViewAttr) { + mMinSamplingDistance = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_gestureTrailMinSamplingDistance, + DEFAULT_MIN_SAMPLING_DISTANCE); + final int interpolationAngularDegree = mainKeyboardViewAttr.getInteger(R.styleable + .MainKeyboardView_gestureTrailMaxInterpolationAngularThreshold, 0); + mMaxInterpolationAngularThreshold = (interpolationAngularDegree <= 0) + ? Math.toRadians(DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD) + : Math.toRadians(interpolationAngularDegree); + mMaxInterpolationDistanceThreshold = mainKeyboardViewAttr.getDimension(R.styleable + .MainKeyboardView_gestureTrailMaxInterpolationDistanceThreshold, + DEFAULT_MAX_INTERPOLATION_DISTANCE_THRESHOLD); + mMaxInterpolationSegments = mainKeyboardViewAttr.getInteger( + R.styleable.MainKeyboardView_gestureTrailMaxInterpolationSegments, + DEFAULT_MAX_INTERPOLATION_SEGMENTS); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeDrawingPoints.java index ecc67dd44..7d09e9d2f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeDrawingPoints.java @@ -16,19 +16,19 @@ package com.android.inputmethod.keyboard.internal; -import android.content.res.TypedArray; - -import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.ResizableIntArray; -public final class GestureStrokeWithPreviewPoints extends GestureStroke { +/** + * This class holds drawing points to represent a gesture stroke on the screen. + */ +public final class GestureStrokeDrawingPoints { public static final int PREVIEW_CAPACITY = 256; private final ResizableIntArray mPreviewEventTimes = new ResizableIntArray(PREVIEW_CAPACITY); private final ResizableIntArray mPreviewXCoordinates = new ResizableIntArray(PREVIEW_CAPACITY); private final ResizableIntArray mPreviewYCoordinates = new ResizableIntArray(PREVIEW_CAPACITY); - private final GestureStrokePreviewParams mPreviewParams; + private final GestureStrokeDrawingParams mDrawingParams; private int mStrokeId; private int mLastPreviewSize; @@ -39,56 +39,11 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { private int mLastY; private double mDistanceFromLastSample; - public static final class GestureStrokePreviewParams { - public final double mMinSamplingDistance; // in pixel - public final double mMaxInterpolationAngularThreshold; // in radian - public final double mMaxInterpolationDistanceThreshold; // in pixel - public final int mMaxInterpolationSegments; - - public static final GestureStrokePreviewParams DEFAULT = new GestureStrokePreviewParams(); - - private static final int DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD = 15; // in degree - - private GestureStrokePreviewParams() { - mMinSamplingDistance = 0.0d; - mMaxInterpolationAngularThreshold = - degreeToRadian(DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD); - mMaxInterpolationDistanceThreshold = mMinSamplingDistance; - mMaxInterpolationSegments = 4; - } - - private static double degreeToRadian(final int degree) { - return degree / 180.0d * Math.PI; - } - - public GestureStrokePreviewParams(final TypedArray mainKeyboardViewAttr) { - mMinSamplingDistance = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_gestureTrailMinSamplingDistance, - (float)DEFAULT.mMinSamplingDistance); - final int interpolationAngularDegree = mainKeyboardViewAttr.getInteger(R.styleable - .MainKeyboardView_gestureTrailMaxInterpolationAngularThreshold, 0); - mMaxInterpolationAngularThreshold = (interpolationAngularDegree <= 0) - ? DEFAULT.mMaxInterpolationAngularThreshold - : degreeToRadian(interpolationAngularDegree); - mMaxInterpolationDistanceThreshold = mainKeyboardViewAttr.getDimension(R.styleable - .MainKeyboardView_gestureTrailMaxInterpolationDistanceThreshold, - (float)DEFAULT.mMaxInterpolationDistanceThreshold); - mMaxInterpolationSegments = mainKeyboardViewAttr.getInteger( - R.styleable.MainKeyboardView_gestureTrailMaxInterpolationSegments, - DEFAULT.mMaxInterpolationSegments); - } - } - - public GestureStrokeWithPreviewPoints(final int pointerId, - final GestureStrokeParams strokeParams, - final GestureStrokePreviewParams previewParams) { - super(pointerId, strokeParams); - mPreviewParams = previewParams; + public GestureStrokeDrawingPoints(final GestureStrokeDrawingParams drawingParams) { + mDrawingParams = drawingParams; } - @Override - protected void reset() { - super.reset(); + private void reset() { mStrokeId++; mLastPreviewSize = 0; mLastInterpolatedPreviewIndex = 0; @@ -101,28 +56,29 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { return mStrokeId; } + public void onDownEvent(final int x, final int y, final int elapsedTimeSinceFirstDown) { + reset(); + onMoveEvent(x, y, elapsedTimeSinceFirstDown); + } + private boolean needsSampling(final int x, final int y) { mDistanceFromLastSample += Math.hypot(x - mLastX, y - mLastY); mLastX = x; mLastY = y; final boolean isDownEvent = (mPreviewEventTimes.getLength() == 0); - if (mDistanceFromLastSample >= mPreviewParams.mMinSamplingDistance || isDownEvent) { + if (mDistanceFromLastSample >= mDrawingParams.mMinSamplingDistance || isDownEvent) { mDistanceFromLastSample = 0.0d; return true; } return false; } - @Override - public boolean addPointOnKeyboard(final int x, final int y, final int time, - final boolean isMajorEvent) { + public void onMoveEvent(final int x, final int y, final int elapsedTimeSinceFirstDown) { if (needsSampling(x, y)) { - mPreviewEventTimes.add(time); + mPreviewEventTimes.add(elapsedTimeSinceFirstDown); mPreviewXCoordinates.add(x); mPreviewYCoordinates.add(y); } - return super.addPointOnKeyboard(x, y, time, isMajorEvent); - } /** @@ -132,7 +88,7 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { * @param xCoords the x-coordinates array of gesture trail to be drawn. * @param yCoords the y-coordinates array of gesture trail to be drawn. * @param types the point types array of gesture trail. This is valid only when - * {@link GestureTrail#DEBUG_SHOW_POINTS} is true. + * {@link GestureTrailDrawingPoints#DEBUG_SHOW_POINTS} is true. */ public void appendPreviewStroke(final ResizableIntArray eventTimes, final ResizableIntArray xCoords, final ResizableIntArray yCoords, @@ -144,8 +100,8 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { eventTimes.append(mPreviewEventTimes, mLastPreviewSize, length); xCoords.append(mPreviewXCoordinates, mLastPreviewSize, length); yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length); - if (GestureTrail.DEBUG_SHOW_POINTS) { - types.fill(GestureTrail.POINT_TYPE_SAMPLED, types.getLength(), length); + if (GestureTrailDrawingPoints.DEBUG_SHOW_POINTS) { + types.fill(GestureTrailDrawingPoints.POINT_TYPE_SAMPLED, types.getLength(), length); } mLastPreviewSize = mPreviewEventTimes.getLength(); } @@ -162,7 +118,7 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { * @param xCoords the x-coordinates array of gesture trail to be drawn. * @param yCoords the y-coordinates array of gesture trail to be drawn. * @param types the point types array of gesture trail. This is valid only when - * {@link GestureTrail#DEBUG_SHOW_POINTS} is true. + * {@link GestureTrailDrawingPoints#DEBUG_SHOW_POINTS} is true. * @return the start index of the last interpolated segment of input arrays. */ public int interpolateStrokeAndReturnStartIndexOfLastSegment(final int lastInterpolatedIndex, @@ -188,12 +144,12 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { final double m2 = Math.atan2(mInterpolator.mSlope2Y, mInterpolator.mSlope2X); final double deltaAngle = Math.abs(angularDiff(m2, m1)); final int segmentsByAngle = (int)Math.ceil( - deltaAngle / mPreviewParams.mMaxInterpolationAngularThreshold); + deltaAngle / mDrawingParams.mMaxInterpolationAngularThreshold); final double deltaDistance = Math.hypot(mInterpolator.mP1X - mInterpolator.mP2X, mInterpolator.mP1Y - mInterpolator.mP2Y); final int segmentsByDistance = (int)Math.ceil(deltaDistance - / mPreviewParams.mMaxInterpolationDistanceThreshold); - final int segments = Math.min(mPreviewParams.mMaxInterpolationSegments, + / mDrawingParams.mMaxInterpolationDistanceThreshold); + final int segments = Math.min(mDrawingParams.mMaxInterpolationSegments, Math.max(segmentsByAngle, segmentsByDistance)); final int t1 = eventTimes.get(d1); final int dt = pt[p2] - pt[p1]; @@ -201,19 +157,19 @@ public final class GestureStrokeWithPreviewPoints extends GestureStroke { for (int i = 1; i < segments; i++) { final float t = i / (float)segments; mInterpolator.interpolate(t); - eventTimes.add(d1, (int)(dt * t) + t1); - xCoords.add(d1, (int)mInterpolator.mInterpolatedX); - yCoords.add(d1, (int)mInterpolator.mInterpolatedY); - if (GestureTrail.DEBUG_SHOW_POINTS) { - types.add(d1, GestureTrail.POINT_TYPE_INTERPOLATED); + eventTimes.addAt(d1, (int)(dt * t) + t1); + xCoords.addAt(d1, (int)mInterpolator.mInterpolatedX); + yCoords.addAt(d1, (int)mInterpolator.mInterpolatedY); + if (GestureTrailDrawingPoints.DEBUG_SHOW_POINTS) { + types.addAt(d1, GestureTrailDrawingPoints.POINT_TYPE_INTERPOLATED); } d1++; } - eventTimes.add(d1, pt[p2]); - xCoords.add(d1, px[p2]); - yCoords.add(d1, py[p2]); - if (GestureTrail.DEBUG_SHOW_POINTS) { - types.add(d1, GestureTrail.POINT_TYPE_SAMPLED); + eventTimes.addAt(d1, pt[p2]); + xCoords.addAt(d1, px[p2]); + yCoords.addAt(d1, py[p2]); + if (GestureTrailDrawingPoints.DEBUG_SHOW_POINTS) { + types.addAt(d1, GestureTrailDrawingPoints.POINT_TYPE_SAMPLED); } } return lastInterpolatedDrawIndex; diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeRecognitionParams.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeRecognitionParams.java new file mode 100644 index 000000000..07b14514c --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeRecognitionParams.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.content.res.TypedArray; + +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.utils.ResourceUtils; + +/** + * This class holds parameters to control how a gesture stroke is sampled and recognized. + * This class also has parameters to distinguish gesture input events from fast typing events. + * + * @attr ref R.styleable#MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping + * @attr ref R.styleable#MainKeyboardView_gestureDetectFastMoveSpeedThreshold + * @attr ref R.styleable#MainKeyboardView_gestureDynamicThresholdDecayDuration + * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdFrom + * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdTo + * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdFrom + * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdTo + * @attr ref R.styleable#MainKeyboardView_gestureSamplingMinimumDistance + * @attr ref R.styleable#MainKeyboardView_gestureRecognitionMinimumTime + * @attr ref R.styleable#MainKeyboardView_gestureRecognitionSpeedThreshold + */ +public final class GestureStrokeRecognitionParams { + // Static threshold for gesture after fast typing + public final int mStaticTimeThresholdAfterFastTyping; // msec + // Static threshold for starting gesture detection + public final float mDetectFastMoveSpeedThreshold; // keyWidth/sec + // Dynamic threshold for gesture after fast typing + public final int mDynamicThresholdDecayDuration; // msec + // Time based threshold values + public final int mDynamicTimeThresholdFrom; // msec + public final int mDynamicTimeThresholdTo; // msec + // Distance based threshold values + public final float mDynamicDistanceThresholdFrom; // keyWidth + public final float mDynamicDistanceThresholdTo; // keyWidth + // Parameters for gesture sampling + public final float mSamplingMinimumDistance; // keyWidth + // Parameters for gesture recognition + public final int mRecognitionMinimumTime; // msec + public final float mRecognitionSpeedThreshold; // keyWidth/sec + + // Default GestureStrokeRecognitionPoints parameters. + public static final GestureStrokeRecognitionParams DEFAULT = + new GestureStrokeRecognitionParams(); + + private GestureStrokeRecognitionParams() { + // These parameter values are default and intended for testing. + mStaticTimeThresholdAfterFastTyping = 350; // msec + mDetectFastMoveSpeedThreshold = 1.5f; // keyWidth/sec + mDynamicThresholdDecayDuration = 450; // msec + mDynamicTimeThresholdFrom = 300; // msec + mDynamicTimeThresholdTo = 20; // msec + mDynamicDistanceThresholdFrom = 6.0f; // keyWidth + mDynamicDistanceThresholdTo = 0.35f; // keyWidth + // The following parameters' change will affect the result of regression test. + mSamplingMinimumDistance = 1.0f / 6.0f; // keyWidth + mRecognitionMinimumTime = 100; // msec + mRecognitionSpeedThreshold = 5.5f; // keyWidth/sec + } + + public GestureStrokeRecognitionParams(final TypedArray mainKeyboardViewAttr) { + mStaticTimeThresholdAfterFastTyping = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping, + DEFAULT.mStaticTimeThresholdAfterFastTyping); + mDetectFastMoveSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr, + R.styleable.MainKeyboardView_gestureDetectFastMoveSpeedThreshold, + DEFAULT.mDetectFastMoveSpeedThreshold); + mDynamicThresholdDecayDuration = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureDynamicThresholdDecayDuration, + DEFAULT.mDynamicThresholdDecayDuration); + mDynamicTimeThresholdFrom = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureDynamicTimeThresholdFrom, + DEFAULT.mDynamicTimeThresholdFrom); + mDynamicTimeThresholdTo = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureDynamicTimeThresholdTo, + DEFAULT.mDynamicTimeThresholdTo); + mDynamicDistanceThresholdFrom = ResourceUtils.getFraction(mainKeyboardViewAttr, + R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdFrom, + DEFAULT.mDynamicDistanceThresholdFrom); + mDynamicDistanceThresholdTo = ResourceUtils.getFraction(mainKeyboardViewAttr, + R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdTo, + DEFAULT.mDynamicDistanceThresholdTo); + mSamplingMinimumDistance = ResourceUtils.getFraction(mainKeyboardViewAttr, + R.styleable.MainKeyboardView_gestureSamplingMinimumDistance, + DEFAULT.mSamplingMinimumDistance); + mRecognitionMinimumTime = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureRecognitionMinimumTime, + DEFAULT.mRecognitionMinimumTime); + mRecognitionSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr, + R.styleable.MainKeyboardView_gestureRecognitionSpeedThreshold, + DEFAULT.mRecognitionSpeedThreshold); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeRecognitionPoints.java index f29ade861..e49e538aa 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeRecognitionPoints.java @@ -16,16 +16,18 @@ package com.android.inputmethod.keyboard.internal; -import android.content.res.TypedArray; import android.util.Log; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.InputPointers; -import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.ResizableIntArray; -import com.android.inputmethod.latin.utils.ResourceUtils; -public class GestureStroke { - private static final String TAG = GestureStroke.class.getSimpleName(); +/** + * This class holds event points to recognize a gesture stroke. + * TODO: Should be package private class. + */ +public final class GestureStrokeRecognitionPoints { + private static final String TAG = GestureStrokeRecognitionPoints.class.getSimpleName(); private static final boolean DEBUG = false; private static final boolean DEBUG_SPEED = false; @@ -33,14 +35,15 @@ public class GestureStroke { // Proportional to the keyboard height. public static final float EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO = 0.25f; - public static final int DEFAULT_CAPACITY = 128; - private final int mPointerId; - private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY); - private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); - private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); + private final ResizableIntArray mEventTimes = new ResizableIntArray( + Constants.DEFAULT_GESTURE_POINTS_CAPACITY); + private final ResizableIntArray mXCoordinates = new ResizableIntArray( + Constants.DEFAULT_GESTURE_POINTS_CAPACITY); + private final ResizableIntArray mYCoordinates = new ResizableIntArray( + Constants.DEFAULT_GESTURE_POINTS_CAPACITY); - private final GestureStrokeParams mParams; + private final GestureStrokeRecognitionParams mRecognitionParams; private int mKeyWidth; // pixel private int mMinYCoordinate; // pixel @@ -64,145 +67,85 @@ public class GestureStroke { private int mIncrementalRecognitionSize; private int mLastIncrementalBatchSize; - public static final class GestureStrokeParams { - // Static threshold for gesture after fast typing - public final int mStaticTimeThresholdAfterFastTyping; // msec - // Static threshold for starting gesture detection - public final float mDetectFastMoveSpeedThreshold; // keyWidth/sec - // Dynamic threshold for gesture after fast typing - public final int mDynamicThresholdDecayDuration; // msec - // Time based threshold values - public final int mDynamicTimeThresholdFrom; // msec - public final int mDynamicTimeThresholdTo; // msec - // Distance based threshold values - public final float mDynamicDistanceThresholdFrom; // keyWidth - public final float mDynamicDistanceThresholdTo; // keyWidth - // Parameters for gesture sampling - public final float mSamplingMinimumDistance; // keyWidth - // Parameters for gesture recognition - public final int mRecognitionMinimumTime; // msec - public final float mRecognitionSpeedThreshold; // keyWidth/sec - - // Default GestureStroke parameters. - public static final GestureStrokeParams DEFAULT = new GestureStrokeParams(); - - private GestureStrokeParams() { - // These parameter values are default and intended for testing. - mStaticTimeThresholdAfterFastTyping = 350; // msec - mDetectFastMoveSpeedThreshold = 1.5f; // keyWidth / sec - mDynamicThresholdDecayDuration = 450; // msec - mDynamicTimeThresholdFrom = 300; // msec - mDynamicTimeThresholdTo = 20; // msec - mDynamicDistanceThresholdFrom = 6.0f; // keyWidth - mDynamicDistanceThresholdTo = 0.35f; // keyWidth - // The following parameters' change will affect the result of regression test. - mSamplingMinimumDistance = 1.0f / 6.0f; // keyWidth - mRecognitionMinimumTime = 100; // msec - mRecognitionSpeedThreshold = 5.5f; // keyWidth / sec - } - - public GestureStrokeParams(final TypedArray mainKeyboardViewAttr) { - mStaticTimeThresholdAfterFastTyping = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping, - DEFAULT.mStaticTimeThresholdAfterFastTyping); - mDetectFastMoveSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr, - R.styleable.MainKeyboardView_gestureDetectFastMoveSpeedThreshold, - DEFAULT.mDetectFastMoveSpeedThreshold); - mDynamicThresholdDecayDuration = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureDynamicThresholdDecayDuration, - DEFAULT.mDynamicThresholdDecayDuration); - mDynamicTimeThresholdFrom = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureDynamicTimeThresholdFrom, - DEFAULT.mDynamicTimeThresholdFrom); - mDynamicTimeThresholdTo = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureDynamicTimeThresholdTo, - DEFAULT.mDynamicTimeThresholdTo); - mDynamicDistanceThresholdFrom = ResourceUtils.getFraction(mainKeyboardViewAttr, - R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdFrom, - DEFAULT.mDynamicDistanceThresholdFrom); - mDynamicDistanceThresholdTo = ResourceUtils.getFraction(mainKeyboardViewAttr, - R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdTo, - DEFAULT.mDynamicDistanceThresholdTo); - mSamplingMinimumDistance = ResourceUtils.getFraction(mainKeyboardViewAttr, - R.styleable.MainKeyboardView_gestureSamplingMinimumDistance, - DEFAULT.mSamplingMinimumDistance); - mRecognitionMinimumTime = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureRecognitionMinimumTime, - DEFAULT.mRecognitionMinimumTime); - mRecognitionSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr, - R.styleable.MainKeyboardView_gestureRecognitionSpeedThreshold, - DEFAULT.mRecognitionSpeedThreshold); - } - } - private static final int MSEC_PER_SEC = 1000; - public GestureStroke(final int pointerId, final GestureStrokeParams params) { + // TODO: Make this package private + public GestureStrokeRecognitionPoints(final int pointerId, + final GestureStrokeRecognitionParams recognitionParams) { mPointerId = pointerId; - mParams = params; + mRecognitionParams = recognitionParams; } + // TODO: Make this package private public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) { mKeyWidth = keyWidth; mMinYCoordinate = -(int)(keyboardHeight * EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO); mMaxYCoordinate = keyboardHeight; // TODO: Find an appropriate base metric for these length. Maybe diagonal length of the key? - mDetectFastMoveSpeedThreshold = (int)(keyWidth * mParams.mDetectFastMoveSpeedThreshold); - mGestureDynamicDistanceThresholdFrom = - (int)(keyWidth * mParams.mDynamicDistanceThresholdFrom); - mGestureDynamicDistanceThresholdTo = (int)(keyWidth * mParams.mDynamicDistanceThresholdTo); - mGestureSamplingMinimumDistance = (int)(keyWidth * mParams.mSamplingMinimumDistance); - mGestureRecognitionSpeedThreshold = (int)(keyWidth * mParams.mRecognitionSpeedThreshold); + mDetectFastMoveSpeedThreshold = (int)( + keyWidth * mRecognitionParams.mDetectFastMoveSpeedThreshold); + mGestureDynamicDistanceThresholdFrom = (int)( + keyWidth * mRecognitionParams.mDynamicDistanceThresholdFrom); + mGestureDynamicDistanceThresholdTo = (int)( + keyWidth * mRecognitionParams.mDynamicDistanceThresholdTo); + mGestureSamplingMinimumDistance = (int)( + keyWidth * mRecognitionParams.mSamplingMinimumDistance); + mGestureRecognitionSpeedThreshold = (int)( + keyWidth * mRecognitionParams.mRecognitionSpeedThreshold); if (DEBUG) { Log.d(TAG, String.format( "[%d] setKeyboardGeometry: keyWidth=%3d tT=%3d >> %3d tD=%3d >> %3d", mPointerId, keyWidth, - mParams.mDynamicTimeThresholdFrom, - mParams.mDynamicTimeThresholdTo, + mRecognitionParams.mDynamicTimeThresholdFrom, + mRecognitionParams.mDynamicTimeThresholdTo, mGestureDynamicDistanceThresholdFrom, mGestureDynamicDistanceThresholdTo)); } } + // TODO: Make this package private public int getLength() { return mEventTimes.getLength(); } - public void onDownEvent(final int x, final int y, final long downTime, - final long gestureFirstDownTime, final long lastTypingTime) { + // TODO: Make this package private + public void addDownEventPoint(final int x, final int y, final int elapsedTimeSinceFirstDown, + final int elapsedTimeSinceLastTyping) { reset(); - final long elapsedTimeAfterTyping = downTime - lastTypingTime; - if (elapsedTimeAfterTyping < mParams.mStaticTimeThresholdAfterFastTyping) { + if (elapsedTimeSinceLastTyping < mRecognitionParams.mStaticTimeThresholdAfterFastTyping) { mAfterFastTyping = true; } if (DEBUG) { Log.d(TAG, String.format("[%d] onDownEvent: dT=%3d%s", mPointerId, - elapsedTimeAfterTyping, mAfterFastTyping ? " afterFastTyping" : "")); + elapsedTimeSinceLastTyping, mAfterFastTyping ? " afterFastTyping" : "")); } - final int elapsedTimeFromFirstDown = (int)(downTime - gestureFirstDownTime); - addPointOnKeyboard(x, y, elapsedTimeFromFirstDown, true /* isMajorEvent */); + // Call {@link #addEventPoint(int,int,int,boolean)} to record this down event point as a + // major event point. + addEventPoint(x, y, elapsedTimeSinceFirstDown, true /* isMajorEvent */); } private int getGestureDynamicDistanceThreshold(final int deltaTime) { - if (!mAfterFastTyping || deltaTime >= mParams.mDynamicThresholdDecayDuration) { + if (!mAfterFastTyping || deltaTime >= mRecognitionParams.mDynamicThresholdDecayDuration) { return mGestureDynamicDistanceThresholdTo; } final int decayedThreshold = (mGestureDynamicDistanceThresholdFrom - mGestureDynamicDistanceThresholdTo) - * deltaTime / mParams.mDynamicThresholdDecayDuration; + * deltaTime / mRecognitionParams.mDynamicThresholdDecayDuration; return mGestureDynamicDistanceThresholdFrom - decayedThreshold; } private int getGestureDynamicTimeThreshold(final int deltaTime) { - if (!mAfterFastTyping || deltaTime >= mParams.mDynamicThresholdDecayDuration) { - return mParams.mDynamicTimeThresholdTo; + if (!mAfterFastTyping || deltaTime >= mRecognitionParams.mDynamicThresholdDecayDuration) { + return mRecognitionParams.mDynamicTimeThresholdTo; } final int decayedThreshold = - (mParams.mDynamicTimeThresholdFrom - mParams.mDynamicTimeThresholdTo) - * deltaTime / mParams.mDynamicThresholdDecayDuration; - return mParams.mDynamicTimeThresholdFrom - decayedThreshold; + (mRecognitionParams.mDynamicTimeThresholdFrom + - mRecognitionParams.mDynamicTimeThresholdTo) + * deltaTime / mRecognitionParams.mDynamicThresholdDecayDuration; + return mRecognitionParams.mDynamicTimeThresholdFrom - decayedThreshold; } + // TODO: Make this package private public final boolean isStartOfAGesture() { if (!hasDetectedFastMove()) { return false; @@ -233,6 +176,7 @@ public class GestureStroke { return isStartOfAGesture; } + // TODO: Make this package private public void duplicateLastPointWith(final int time) { final int lastIndex = getLength() - 1; if (lastIndex >= 0) { @@ -248,7 +192,7 @@ public class GestureStroke { } } - protected void reset() { + private void reset() { mIncrementalRecognitionSize = 0; mLastIncrementalBatchSize = 0; mEventTimes.setLength(0); @@ -316,19 +260,20 @@ public class GestureStroke { } /** - * Add a touch event as a gesture point. Returns true if the touch event is on the valid - * gesture area. - * @param x the x-coordinate of the touch event - * @param y the y-coordinate of the touch event + * Add an event point to this gesture stroke recognition points. Returns true if the event + * point is on the valid gesture area. + * @param x the x-coordinate of the event point + * @param y the y-coordinate of the event point * @param time the elapsed time in millisecond from the first gesture down * @param isMajorEvent false if this is a historical move event - * @return true if the touch event is on the valid gesture area + * @return true if the event point is on the valid gesture area */ - public boolean addPointOnKeyboard(final int x, final int y, final int time, + // TODO: Make this package private + public boolean addEventPoint(final int x, final int y, final int time, final boolean isMajorEvent) { final int size = getLength(); if (size <= 0) { - // Down event + // The first event of this stroke (a.k.a. down event). appendPoint(x, y, time); updateMajorEvent(x, y, time); } else { @@ -357,15 +302,18 @@ public class GestureStroke { } } + // TODO: Make this package private public final boolean hasRecognitionTimePast( final long currentTime, final long lastRecognitionTime) { - return currentTime > lastRecognitionTime + mParams.mRecognitionMinimumTime; + return currentTime > lastRecognitionTime + mRecognitionParams.mRecognitionMinimumTime; } + // TODO: Make this package private public final void appendAllBatchPoints(final InputPointers out) { appendBatchPoints(out, getLength()); } + // TODO: Make this package private public final void appendIncrementalBatchPoints(final InputPointers out) { appendBatchPoints(out, mIncrementalRecognitionSize); } @@ -381,10 +329,6 @@ public class GestureStroke { } private static int getDistance(final int x1, final int y1, final int x2, final int y2) { - final int dx = x1 - x2; - final int dy = y1 - y2; - // Note that, in recent versions of Android, FloatMath is actually slower than - // java.lang.Math due to the way the JIT optimizes java.lang.Math. - return (int)Math.sqrt(dx * dx + dy * dy); + return (int)Math.hypot(x1 - x2, y1 - y2); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailDrawingParams.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailDrawingParams.java new file mode 100644 index 000000000..088f03aa6 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailDrawingParams.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.content.res.TypedArray; + +import com.android.inputmethod.latin.R; + +/** + * This class holds parameters to control how a gesture trail is drawn and animated on the screen. + * + * On the other hand, {@link GestureStrokeDrawingParams} class controls how each gesture stroke is + * sampled and interpolated. This class controls how those gesture strokes are displayed as a + * gesture trail and animated on the screen. + * + * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutStartDelay + * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutDuration + * @attr ref R.styleable#MainKeyboardView_gestureTrailUpdateInterval + * @attr ref R.styleable#MainKeyboardView_gestureTrailColor + * @attr ref R.styleable#MainKeyboardView_gestureTrailWidth + */ +final class GestureTrailDrawingParams { + private static final int FADEOUT_START_DELAY_FOR_DEBUG = 2000; // millisecond + private static final int FADEOUT_DURATION_FOR_DEBUG = 200; // millisecond + + public final int mTrailColor; + public final float mTrailStartWidth; + public final float mTrailEndWidth; + public final float mTrailBodyRatio; + public boolean mTrailShadowEnabled; + public final float mTrailShadowRatio; + public final int mFadeoutStartDelay; + public final int mFadeoutDuration; + public final int mUpdateInterval; + + public final int mTrailLingerDuration; + + public GestureTrailDrawingParams(final TypedArray mainKeyboardViewAttr) { + mTrailColor = mainKeyboardViewAttr.getColor( + R.styleable.MainKeyboardView_gestureTrailColor, 0); + mTrailStartWidth = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_gestureTrailStartWidth, 0.0f); + mTrailEndWidth = mainKeyboardViewAttr.getDimension( + R.styleable.MainKeyboardView_gestureTrailEndWidth, 0.0f); + final int PERCENTAGE_INT = 100; + mTrailBodyRatio = (float)mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureTrailBodyRatio, PERCENTAGE_INT) + / (float)PERCENTAGE_INT; + final int trailShadowRatioInt = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureTrailShadowRatio, 0); + mTrailShadowEnabled = (trailShadowRatioInt > 0); + mTrailShadowRatio = (float)trailShadowRatioInt / (float)PERCENTAGE_INT; + mFadeoutStartDelay = GestureTrailDrawingPoints.DEBUG_SHOW_POINTS + ? FADEOUT_START_DELAY_FOR_DEBUG + : mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureTrailFadeoutStartDelay, 0); + mFadeoutDuration = GestureTrailDrawingPoints.DEBUG_SHOW_POINTS + ? FADEOUT_DURATION_FOR_DEBUG + : mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureTrailFadeoutDuration, 0); + mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration; + mUpdateInterval = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_gestureTrailUpdateInterval, 0); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailDrawingPoints.java index aca667919..bf4c4da10 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailDrawingPoints.java @@ -16,7 +16,6 @@ package com.android.inputmethod.keyboard.internal; -import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; @@ -25,24 +24,22 @@ import android.graphics.Rect; import android.os.SystemClock; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.ResizableIntArray; -/* - * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutStartDelay - * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutDuration - * @attr ref R.styleable#MainKeyboardView_gestureTrailUpdateInterval - * @attr ref R.styleable#MainKeyboardView_gestureTrailColor - * @attr ref R.styleable#MainKeyboardView_gestureTrailWidth +/** + * This class holds drawing points to represent a gesture trail. The gesture trail may contain + * multiple non-contiguous gesture strokes and will be animated asynchronously from gesture input. + * + * On the other hand, {@link GestureStrokeDrawingPoints} class holds drawing points of each gesture + * stroke. This class holds drawing points of those gesture strokes to draw as a gesture trail. + * Drawing points in this class will be asynchronously removed when fading out animation goes. */ -final class GestureTrail { +final class GestureTrailDrawingPoints { public static final boolean DEBUG_SHOW_POINTS = false; public static final int POINT_TYPE_SAMPLED = 1; public static final int POINT_TYPE_INTERPOLATED = 2; - private static final int FADEOUT_START_DELAY_FOR_DEBUG = 2000; // millisecond - private static final int FADEOUT_DURATION_FOR_DEBUG = 200; // millisecond - private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY; + private static final int DEFAULT_CAPACITY = GestureStrokeDrawingPoints.PREVIEW_CAPACITY; // These three {@link ResizableIntArray}s should be synchronized by {@link #mEventTimes}. private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); @@ -56,46 +53,6 @@ final class GestureTrail { private int mTrailStartIndex; private int mLastInterpolatedDrawIndex; - static final class Params { - public final int mTrailColor; - public final float mTrailStartWidth; - public final float mTrailEndWidth; - public final float mTrailBodyRatio; - public boolean mTrailShadowEnabled; - public final float mTrailShadowRatio; - public final int mFadeoutStartDelay; - public final int mFadeoutDuration; - public final int mUpdateInterval; - - public final int mTrailLingerDuration; - - public Params(final TypedArray mainKeyboardViewAttr) { - mTrailColor = mainKeyboardViewAttr.getColor( - R.styleable.MainKeyboardView_gestureTrailColor, 0); - mTrailStartWidth = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_gestureTrailStartWidth, 0.0f); - mTrailEndWidth = mainKeyboardViewAttr.getDimension( - R.styleable.MainKeyboardView_gestureTrailEndWidth, 0.0f); - final int PERCENTAGE_INT = 100; - mTrailBodyRatio = (float)mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureTrailBodyRatio, PERCENTAGE_INT) - / (float)PERCENTAGE_INT; - final int trailShadowRatioInt = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureTrailShadowRatio, 0); - mTrailShadowEnabled = (trailShadowRatioInt > 0); - mTrailShadowRatio = (float)trailShadowRatioInt / (float)PERCENTAGE_INT; - mFadeoutStartDelay = DEBUG_SHOW_POINTS ? FADEOUT_START_DELAY_FOR_DEBUG - : mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureTrailFadeoutStartDelay, 0); - mFadeoutDuration = DEBUG_SHOW_POINTS ? FADEOUT_DURATION_FOR_DEBUG - : mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureTrailFadeoutDuration, 0); - mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration; - mUpdateInterval = mainKeyboardViewAttr.getInt( - R.styleable.MainKeyboardView_gestureTrailUpdateInterval, 0); - } - } - // Use this value as imaginary zero because x-coordinates may be zero. private static final int DOWN_EVENT_MARKER = -128; @@ -112,13 +69,13 @@ final class GestureTrail { ? DOWN_EVENT_MARKER - xCoordOrMark : xCoordOrMark; } - public void addStroke(final GestureStrokeWithPreviewPoints stroke, final long downTime) { + public void addStroke(final GestureStrokeDrawingPoints stroke, final long downTime) { synchronized (mEventTimes) { addStrokeLocked(stroke, downTime); } } - private void addStrokeLocked(final GestureStrokeWithPreviewPoints stroke, final long downTime) { + private void addStrokeLocked(final GestureStrokeDrawingPoints stroke, final long downTime) { final int trailSize = mEventTimes.getLength(); stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates, mPointTypes); if (mEventTimes.getLength() == trailSize) { @@ -126,13 +83,14 @@ final class GestureTrail { } final int[] eventTimes = mEventTimes.getPrimitiveArray(); final int strokeId = stroke.getGestureStrokeId(); - // Because interpolation algorithm in {@link GestureStrokeWithPreviewPoints} can't determine + // Because interpolation algorithm in {@link GestureStrokeDrawingPoints} can't determine // the interpolated points in the last segment of gesture stroke, it may need recalculation // of interpolation when new segments are added to the stroke. // {@link #mLastInterpolatedDrawIndex} holds the start index of the last segment. It may // be updated by the interpolation - // {@link GestureStrokeWithPreviewPoints#interpolatePreviewStroke} - // or by animation {@link #drawGestureTrail(Canvas,Paint,Rect,Params)} below. + // {@link GestureStrokeDrawingPoints#interpolatePreviewStroke} + // or by animation {@link #drawGestureTrail(Canvas,Paint,Rect,GestureTrailDrawingParams)} + // below. final int lastInterpolatedIndex = (strokeId == mCurrentStrokeId) ? mLastInterpolatedDrawIndex : trailSize; mLastInterpolatedDrawIndex = stroke.interpolateStrokeAndReturnStartIndexOfLastSegment( @@ -161,7 +119,7 @@ final class GestureTrail { * @param params gesture trail display parameters * @return the width of a gesture trail */ - private static int getAlpha(final int elapsedTime, final Params params) { + private static int getAlpha(final int elapsedTime, final GestureTrailDrawingParams params) { if (elapsedTime < params.mFadeoutStartDelay) { return Constants.Color.ALPHA_OPAQUE; } @@ -180,7 +138,7 @@ final class GestureTrail { * @param params gesture trail display parameters * @return the width of a gesture trail */ - private static float getWidth(final int elapsedTime, final Params params) { + private static float getWidth(final int elapsedTime, final GestureTrailDrawingParams params) { final float deltaWidth = params.mTrailStartWidth - params.mTrailEndWidth; return params.mTrailStartWidth - (deltaWidth * elapsedTime) / params.mTrailLingerDuration; } @@ -197,14 +155,14 @@ final class GestureTrail { * @return true if some gesture trails remain to be drawn */ public boolean drawGestureTrail(final Canvas canvas, final Paint paint, - final Rect outBoundsRect, final Params params) { + final Rect outBoundsRect, final GestureTrailDrawingParams params) { synchronized (mEventTimes) { return drawGestureTrailLocked(canvas, paint, outBoundsRect, params); } } private boolean drawGestureTrailLocked(final Canvas canvas, final Paint paint, - final Rect outBoundsRect, final Params params) { + final Rect outBoundsRect, final GestureTrailDrawingParams params) { // Initialize bounds rectangle. outBoundsRect.setEmpty(); final int trailSize = mEventTimes.getLength(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsDrawingPreview.java index 19e995548..d8b00c707 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java +++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsDrawingPreview.java @@ -29,16 +29,16 @@ import android.util.SparseArray; import android.view.View; import com.android.inputmethod.keyboard.PointerTracker; -import com.android.inputmethod.keyboard.internal.GestureTrail.Params; import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper; +import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; /** - * Draw gesture trail preview graphics during gesture. + * Draw preview graphics of multiple gesture trails during gesture input. */ -public final class GestureTrailsPreview extends AbstractDrawingPreview { - private final SparseArray<GestureTrail> mGestureTrails = CollectionUtils.newSparseArray(); - private final Params mGestureTrailParams; +public final class GestureTrailsDrawingPreview extends AbstractDrawingPreview { + private final SparseArray<GestureTrailDrawingPoints> mGestureTrails = + CollectionUtils.newSparseArray(); + private final GestureTrailDrawingParams mDrawingParams; private final Paint mGesturePaint; private int mOffscreenWidth; private int mOffscreenHeight; @@ -52,21 +52,23 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview { private final DrawingHandler mDrawingHandler; private static final class DrawingHandler - extends StaticInnerHandlerWrapper<GestureTrailsPreview> { + extends LeakGuardHandlerWrapper<GestureTrailsDrawingPreview> { private static final int MSG_UPDATE_GESTURE_TRAIL = 0; - private final Params mGestureTrailParams; + private final GestureTrailDrawingParams mDrawingParams; - public DrawingHandler(final GestureTrailsPreview outerInstance, - final Params gestureTrailParams) { - super(outerInstance); - mGestureTrailParams = gestureTrailParams; + public DrawingHandler(final GestureTrailsDrawingPreview ownerInstance, + final GestureTrailDrawingParams drawingParams) { + super(ownerInstance); + mDrawingParams = drawingParams; } @Override public void handleMessage(final Message msg) { - final GestureTrailsPreview preview = getOuterInstance(); - if (preview == null) return; + final GestureTrailsDrawingPreview preview = getOwnerInstance(); + if (preview == null) { + return; + } switch (msg.what) { case MSG_UPDATE_GESTURE_TRAIL: preview.getDrawingView().invalidate(); @@ -77,14 +79,15 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview { public void postUpdateGestureTrailPreview() { removeMessages(MSG_UPDATE_GESTURE_TRAIL); sendMessageDelayed(obtainMessage(MSG_UPDATE_GESTURE_TRAIL), - mGestureTrailParams.mUpdateInterval); + mDrawingParams.mUpdateInterval); } } - public GestureTrailsPreview(final View drawingView, final TypedArray mainKeyboardViewAttr) { + public GestureTrailsDrawingPreview(final View drawingView, + final TypedArray mainKeyboardViewAttr) { super(drawingView); - mGestureTrailParams = new Params(mainKeyboardViewAttr); - mDrawingHandler = new DrawingHandler(this, mGestureTrailParams); + mDrawingParams = new GestureTrailDrawingParams(mainKeyboardViewAttr); + mDrawingHandler = new DrawingHandler(this, mDrawingParams); final Paint gesturePaint = new Paint(); gesturePaint.setAntiAlias(true); gesturePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); @@ -92,19 +95,17 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview { } @Override - public void setKeyboardGeometry(final int[] originCoords, final int width, final int height) { - mOffscreenOffsetY = (int)( - height * GestureStroke.EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO); + public void setKeyboardViewGeometry(final int[] originCoords, final int width, + final int height) { + super.setKeyboardViewGeometry(originCoords, width, height); + mOffscreenOffsetY = (int)(height + * GestureStrokeRecognitionPoints.EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO); mOffscreenWidth = width; mOffscreenHeight = mOffscreenOffsetY + height; } @Override - public void onDetachFromWindow() { - freeOffscreenBuffer(); - } - - public void deallocateMemory() { + public void onDeallocateMemory() { freeOffscreenBuffer(); } @@ -144,9 +145,9 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview { // Trails count == fingers count that have ever been active. final int trailsCount = mGestureTrails.size(); for (int index = 0; index < trailsCount; index++) { - final GestureTrail trail = mGestureTrails.valueAt(index); + final GestureTrailDrawingPoints trail = mGestureTrails.valueAt(index); needsUpdatingGestureTrail |= trail.drawGestureTrail(offscreenCanvas, paint, - mGestureTrailBoundsRect, mGestureTrailParams); + mGestureTrailBoundsRect, mDrawingParams); // {@link #mGestureTrailBoundsRect} has bounding box of the trail. dirtyRect.union(mGestureTrailBoundsRect); } @@ -189,15 +190,15 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview { if (!isPreviewEnabled()) { return; } - GestureTrail trail; + GestureTrailDrawingPoints trail; synchronized (mGestureTrails) { trail = mGestureTrails.get(tracker.mPointerId); if (trail == null) { - trail = new GestureTrail(); + trail = new GestureTrailDrawingPoints(); mGestureTrails.put(tracker.mPointerId, trail); } } - trail.addStroke(tracker.getGestureStrokeWithPreviewPoints(), tracker.getDownTime()); + trail.addStroke(tracker.getGestureStrokeDrawingPoints(), tracker.getDownTime()); // TODO: Should narrow the invalidate region. getDrawingView().invalidate(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java index b528b692e..1716fa049 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java @@ -16,7 +16,6 @@ package com.android.inputmethod.keyboard.internal; -import android.content.res.ColorStateList; import android.graphics.Typeface; import com.android.inputmethod.latin.utils.ResourceUtils; @@ -33,7 +32,7 @@ public final class KeyDrawParams { public int mHintLabelSize; public int mPreviewTextSize; - public ColorStateList mTextColorStateList; + public int mTextColor; public int mTextInactivatedColor; public int mTextShadowColor; public int mHintLetterColor; @@ -58,7 +57,7 @@ public final class KeyDrawParams { mHintLabelSize = copyFrom.mHintLabelSize; mPreviewTextSize = copyFrom.mPreviewTextSize; - mTextColorStateList = copyFrom.mTextColorStateList; + mTextColor = copyFrom.mTextColor; mTextInactivatedColor = copyFrom.mTextInactivatedColor; mTextShadowColor = copyFrom.mTextShadowColor; mHintLetterColor = copyFrom.mHintLetterColor; @@ -90,8 +89,8 @@ public final class KeyDrawParams { attr.mShiftedLetterHintRatio, mShiftedLetterHintSize); mHintLabelSize = selectTextSize(keyHeight, attr.mHintLabelRatio, mHintLabelSize); mPreviewTextSize = selectTextSize(keyHeight, attr.mPreviewTextRatio, mPreviewTextSize); - mTextColorStateList = - attr.mTextColorStateList != null ? attr.mTextColorStateList : mTextColorStateList; + + mTextColor = selectColor(attr.mTextColor, mTextColor); mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor); mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor); mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java new file mode 100644 index 000000000..625d1f0a4 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java @@ -0,0 +1,285 @@ +/* + * 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.keyboard.internal; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.TextView; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.CoordinateUtils; +import com.android.inputmethod.latin.utils.ViewLayoutUtils; + +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; + +/** + * This class controls pop up key previews. This class decides: + * - what kind of key previews should be shown. + * - where key previews should be placed. + * - how key previews should be shown and dismissed. + */ +public final class KeyPreviewChoreographer { + // Free {@link TextView} pool that can be used for key preview. + private final ArrayDeque<TextView> mFreeKeyPreviewTextViews = CollectionUtils.newArrayDeque(); + // Map from {@link Key} to {@link TextView} that is currently being displayed as key preview. + private final HashMap<Key,TextView> mShowingKeyPreviewTextViews = CollectionUtils.newHashMap(); + + private final KeyPreviewDrawParams mParams; + + public KeyPreviewChoreographer(final KeyPreviewDrawParams params) { + mParams = params; + } + + public TextView getKeyPreviewTextView(final Key key, final ViewGroup placerView) { + TextView previewTextView = mShowingKeyPreviewTextViews.remove(key); + if (previewTextView != null) { + return previewTextView; + } + previewTextView = mFreeKeyPreviewTextViews.poll(); + if (previewTextView != null) { + return previewTextView; + } + final Context context = placerView.getContext(); + if (mParams.mLayoutId != 0) { + previewTextView = (TextView)LayoutInflater.from(context) + .inflate(mParams.mLayoutId, null); + } else { + previewTextView = new TextView(context); + } + placerView.addView(previewTextView, ViewLayoutUtils.newLayoutParam(placerView, 0, 0)); + return previewTextView; + } + + public boolean isShowingKeyPreview(final Key key) { + return mShowingKeyPreviewTextViews.containsKey(key); + } + + public void dismissAllKeyPreviews() { + for (final Key key : new HashSet<Key>(mShowingKeyPreviewTextViews.keySet())) { + dismissKeyPreview(key, false /* withAnimation */); + } + } + + public void dismissKeyPreview(final Key key, final boolean withAnimation) { + if (key == null) { + return; + } + final TextView previewTextView = mShowingKeyPreviewTextViews.get(key); + if (previewTextView == null) { + return; + } + final Object tag = previewTextView.getTag(); + if (withAnimation) { + if (tag instanceof KeyPreviewAnimations) { + final KeyPreviewAnimations animation = (KeyPreviewAnimations)tag; + animation.startDismiss(); + return; + } + } + // Dismiss preview without animation. + mShowingKeyPreviewTextViews.remove(key); + if (tag instanceof Animator) { + ((Animator)tag).cancel(); + } + previewTextView.setTag(null); + previewTextView.setVisibility(View.INVISIBLE); + mFreeKeyPreviewTextViews.add(previewTextView); + } + + // Background state set + private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = { + { // STATE_MIDDLE + {}, + { R.attr.state_has_morekeys } + }, + { // STATE_LEFT + { R.attr.state_left_edge }, + { R.attr.state_left_edge, R.attr.state_has_morekeys } + }, + { // STATE_RIGHT + { R.attr.state_right_edge }, + { R.attr.state_right_edge, R.attr.state_has_morekeys } + } + }; + private static final int STATE_MIDDLE = 0; + private static final int STATE_LEFT = 1; + private static final int STATE_RIGHT = 2; + private static final int STATE_NORMAL = 0; + private static final int STATE_HAS_MOREKEYS = 1; + + public void placeKeyPreview(final Key key, final TextView previewTextView, + final KeyboardIconsSet iconsSet, final KeyDrawParams drawParams, + final int keyboardViewWidth, final int[] originCoords) { + previewTextView.setTextColor(drawParams.mPreviewTextColor); + final Drawable background = previewTextView.getBackground(); + final String label = key.getPreviewLabel(); + // What we show as preview should match what we show on a key top in onDraw(). + if (label != null) { + // TODO Should take care of temporaryShiftLabel here. + previewTextView.setCompoundDrawables(null, null, null, null); + previewTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + key.selectPreviewTextSize(drawParams)); + previewTextView.setTypeface(key.selectPreviewTypeface(drawParams)); + previewTextView.setText(label); + } else { + previewTextView.setCompoundDrawables(null, null, null, key.getPreviewIcon(iconsSet)); + previewTextView.setText(null); + } + + previewTextView.measure( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + mParams.setGeometry(previewTextView); + final int previewWidth = previewTextView.getMeasuredWidth(); + final int previewHeight = mParams.mPreviewHeight; + final int keyDrawWidth = key.getDrawWidth(); + // The key preview is horizontally aligned with the center of the visible part of the + // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and + // the left/right background is used if such background is specified. + final int statePosition; + int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 + + CoordinateUtils.x(originCoords); + if (previewX < 0) { + previewX = 0; + statePosition = STATE_LEFT; + } else if (previewX > keyboardViewWidth - previewWidth) { + previewX = keyboardViewWidth - previewWidth; + statePosition = STATE_RIGHT; + } else { + statePosition = STATE_MIDDLE; + } + // The key preview is placed vertically above the top edge of the parent key with an + // arbitrary offset. + final int previewY = key.getY() - previewHeight + mParams.mPreviewOffset + + CoordinateUtils.y(originCoords); + + if (background != null) { + final int hasMoreKeys = (key.getMoreKeys() != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL; + background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]); + } + ViewLayoutUtils.placeViewAt( + previewTextView, previewX, previewY, previewWidth, previewHeight); + previewTextView.setPivotX(previewWidth / 2.0f); + previewTextView.setPivotY(previewHeight); + } + + public void showKeyPreview(final Key key, final TextView previewTextView, + final boolean withAnimation) { + if (!withAnimation) { + previewTextView.setVisibility(View.VISIBLE); + mShowingKeyPreviewTextViews.put(key, previewTextView); + return; + } + + // Show preview with animation. + final Animator showUpAnimation = createShowUpAniation(key, previewTextView); + final Animator dismissAnimation = createDismissAnimation(key, previewTextView); + final KeyPreviewAnimations animation = new KeyPreviewAnimations( + showUpAnimation, dismissAnimation); + previewTextView.setTag(animation); + animation.startShowUp(); + } + + private static final float KEY_PREVIEW_SHOW_UP_END_SCALE = 1.0f; + private static final AccelerateInterpolator ACCELERATE_INTERPOLATOR = + new AccelerateInterpolator(); + private static final DecelerateInterpolator DECELERATE_INTERPOLATOR = + new DecelerateInterpolator(); + + private Animator createShowUpAniation(final Key key, final TextView previewTextView) { + // TODO: Optimization for no scale animation and no duration. + final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat( + previewTextView, View.SCALE_X, mParams.getShowUpStartScale(), + KEY_PREVIEW_SHOW_UP_END_SCALE); + final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat( + previewTextView, View.SCALE_Y, mParams.getShowUpStartScale(), + KEY_PREVIEW_SHOW_UP_END_SCALE); + final AnimatorSet showUpAnimation = new AnimatorSet(); + showUpAnimation.play(scaleXAnimation).with(scaleYAnimation); + showUpAnimation.setDuration(mParams.getShowUpDuration()); + showUpAnimation.setInterpolator(DECELERATE_INTERPOLATOR); + showUpAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(final Animator animation) { + showKeyPreview(key, previewTextView, false /* withAnimation */); + } + }); + return showUpAnimation; + } + + private Animator createDismissAnimation(final Key key, final TextView previewTextView) { + // TODO: Optimization for no scale animation and no duration. + final ObjectAnimator scaleXAnimation = ObjectAnimator.ofFloat( + previewTextView, View.SCALE_X, mParams.getDismissEndScale()); + final ObjectAnimator scaleYAnimation = ObjectAnimator.ofFloat( + previewTextView, View.SCALE_Y, mParams.getDismissEndScale()); + final AnimatorSet dismissAnimation = new AnimatorSet(); + dismissAnimation.play(scaleXAnimation).with(scaleYAnimation); + final int dismissDuration = Math.min( + mParams.getDismissDuration(), mParams.getLingerTimeout()); + dismissAnimation.setDuration(dismissDuration); + dismissAnimation.setInterpolator(ACCELERATE_INTERPOLATOR); + dismissAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + dismissKeyPreview(key, false /* withAnimation */); + } + }); + return dismissAnimation; + } + + private static class KeyPreviewAnimations extends AnimatorListenerAdapter { + private final Animator mShowUpAnimation; + private final Animator mDismissAnimation; + + public KeyPreviewAnimations(final Animator showUpAnimation, + final Animator dismissAnimation) { + mShowUpAnimation = showUpAnimation; + mDismissAnimation = dismissAnimation; + } + + public void startShowUp() { + mShowUpAnimation.start(); + } + + public void startDismiss() { + if (mShowUpAnimation.isRunning()) { + mShowUpAnimation.addListener(this); + return; + } + mDismissAnimation.start(); + } + + @Override + public void onAnimationEnd(final Animator animation) { + mDismissAnimation.start(); + } + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java index 609d1a57f..37e5c889d 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java @@ -16,7 +16,23 @@ package com.android.inputmethod.keyboard.internal; +import android.content.res.TypedArray; +import android.view.View; + +import com.android.inputmethod.latin.R; + public final class KeyPreviewDrawParams { + // XML attributes of {@link MainKeyboardView}. + public final int mLayoutId; + public final int mPreviewOffset; + public final int mPreviewHeight; + private int mShowUpDuration; + private int mDismissDuration; + private float mShowUpStartScale; + private float mDismissEndScale; + private int mLingerTimeout; + private boolean mShowPopup = true; + // The graphical geometry of the key preview. // <-width-> // +-------+ ^ @@ -34,11 +50,92 @@ public final class KeyPreviewDrawParams { // paddings. To align the more keys keyboard panel's visible part with the visible part of // the background, we need to record the width and height of key preview that don't include // invisible paddings. - public int mPreviewVisibleWidth; - public int mPreviewVisibleHeight; + private int mVisibleWidth; + private int mVisibleHeight; // The key preview may have an arbitrary offset and its background that may have a bottom // padding. To align the more keys keyboard and the key preview we also need to record the // offset between the top edge of parent key and the bottom of the visible part of key // preview background. - public int mPreviewVisibleOffset; + private int mVisibleOffset; + + public KeyPreviewDrawParams(final TypedArray mainKeyboardViewAttr) { + mPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset( + R.styleable.MainKeyboardView_keyPreviewOffset, 0); + mPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize( + R.styleable.MainKeyboardView_keyPreviewHeight, 0); + mLingerTimeout = mainKeyboardViewAttr.getInt( + R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0); + mLayoutId = mainKeyboardViewAttr.getResourceId( + R.styleable.MainKeyboardView_keyPreviewLayout, 0); + if (mLayoutId == 0) { + mShowPopup = false; + } + } + + public void setVisibleOffset(final int previewVisibleOffset) { + mVisibleOffset = previewVisibleOffset; + } + + public int getVisibleOffset() { + return mVisibleOffset; + } + + public void setGeometry(final View previewTextView) { + final int previewWidth = previewTextView.getMeasuredWidth(); + final int previewHeight = mPreviewHeight; + // The width and height of visible part of the key preview background. The content marker + // of the background 9-patch have to cover the visible part of the background. + mVisibleWidth = previewWidth - previewTextView.getPaddingLeft() + - previewTextView.getPaddingRight(); + mVisibleHeight = previewHeight - previewTextView.getPaddingTop() + - previewTextView.getPaddingBottom(); + // The distance between the top edge of the parent key and the bottom of the visible part + // of the key preview background. + setVisibleOffset(mPreviewOffset - previewTextView.getPaddingBottom()); + } + + public int getVisibleWidth() { + return mVisibleWidth; + } + + public int getVisibleHeight() { + return mVisibleHeight; + } + + public void setPopupEnabled(final boolean enabled, final int lingerTimeout) { + mShowPopup = enabled; + mLingerTimeout = lingerTimeout; + } + + public boolean isPopupEnabled() { + return mShowPopup; + } + + public int getLingerTimeout() { + return mLingerTimeout; + } + + public void setAnimationParams(final float showUpStartScale, final int showUpDuration, + final float dismissEndScale, final int dismissDuration) { + mShowUpStartScale = showUpStartScale; + mShowUpDuration = showUpDuration; + mDismissEndScale = dismissEndScale; + mDismissDuration = dismissDuration; + } + + public float getShowUpStartScale() { + return mShowUpStartScale; + } + + public int getShowUpDuration() { + return mShowUpDuration; + } + + public float getDismissEndScale() { + return mDismissEndScale; + } + + public int getDismissDuration() { + return mDismissDuration; + } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java index 22f5b3dd1..48ba8e051 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java @@ -19,114 +19,54 @@ package com.android.inputmethod.keyboard.internal; import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT; import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED; -import android.text.TextUtils; - import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.LatinImeLogger; -import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.StringUtils; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; - /** - * The string parser of more keys specification. - * The specification is comma separated texts each of which represents one "more key". - * The specification might have label or string resource reference in it. These references are - * expanded before parsing comma. - * - Label reference should be a string representation of label (!text/label_name) - * - String resource reference should be a string representation of resource (!text/resource_name) - * Each "more key" specification is one of the following: - * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText). - * - Icon followed by keyOutputText or code (!icon/icon_name|!code/code_name) - * - Icon should be a string representation of icon (!icon/icon_name). - * - Code should be a code point presented by hexadecimal string prefixed with "0x", or a string - * representation of code (!code/code_name). + * The string parser of the key specification. + * + * Each key specification is one of the following: + * - Label optionally followed by keyOutputText (keyLabel|keyOutputText). + * - Label optionally followed by code point (keyLabel|!code/code_name). + * - Icon followed by keyOutputText (!icon/icon_name|keyOutputText). + * - Icon followed by code point (!icon/icon_name|!code/code_name). + * Label and keyOutputText are one of the following: + * - Literal string. + * - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}. + * - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}. + * Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}. + * Code is one of the following: + * - Code point presented by hexadecimal string prefixed with "0x" + * - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}. * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character. - * Note that the '\' is also parsed by XML parser and CSV parser as well. - * See {@link KeyboardIconsSet} about icon_name. + * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)} + * as well. */ +// TODO: Rename to KeySpec and make this class to the key specification object. public final class KeySpecParser { - private static final boolean DEBUG = LatinImeLogger.sDBG; - - private static final int MAX_STRING_REFERENCE_INDIRECTION = 10; - // Constants for parsing. - private static final char COMMA = ','; - private static final char BACKSLASH = '\\'; - private static final char VERTICAL_BAR = '|'; - private static final String PREFIX_TEXT = "!text/"; - static final String PREFIX_ICON = "!icon/"; - private static final String PREFIX_CODE = "!code/"; + private static final char BACKSLASH = Constants.CODE_BACKSLASH; + private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR; private static final String PREFIX_HEX = "0x"; - private static final String ADDITIONAL_MORE_KEY_MARKER = "%"; private KeySpecParser() { // Intentional empty constructor for utility class. } - /** - * Split the text containing multiple key specifications separated by commas into an array of - * key specifications. - * A key specification can contain a character escaped by the backslash character, including a - * comma character. - * Note that an empty key specification will be eliminated from the result array. - * - * @param text the text containing multiple key specifications. - * @return an array of key specification text. Null if the specified <code>text</code> is empty - * or has no key specifications. - */ - public static String[] splitKeySpecs(final String text) { - final int size = text.length(); - if (size == 0) { - return null; - } - // Optimization for one-letter key specification. - if (size == 1) { - return text.charAt(0) == COMMA ? null : new String[] { text }; - } + private static boolean hasIcon(final String keySpec) { + return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON); + } - ArrayList<String> list = null; - int start = 0; - // The characters in question in this loop are COMMA and BACKSLASH. These characters never - // match any high or low surrogate character. So it is OK to iterate through with char - // index. - for (int pos = 0; pos < size; pos++) { - final char c = text.charAt(pos); - if (c == COMMA) { - // Skip empty entry. - if (pos - start > 0) { - if (list == null) { - list = CollectionUtils.newArrayList(); - } - list.add(text.substring(start, pos)); - } - // Skip comma - start = pos + 1; - } else if (c == BACKSLASH) { - // Skip escape character and escaped character. - pos++; - } - } - final String remain = (size - start > 0) ? text.substring(start) : null; - if (list == null) { - return remain != null ? new String[] { remain } : null; + private static boolean hasCode(final String keySpec, final int labelEnd) { + if (labelEnd <= 0 || labelEnd + 1 >= keySpec.length()) { + return false; } - if (remain != null) { - list.add(remain); + if (keySpec.startsWith(KeyboardCodesSet.PREFIX_CODE, labelEnd + 1)) { + return true; } - return list.toArray(new String[list.size()]); - } - - private static boolean hasIcon(final String moreKeySpec) { - return moreKeySpec.startsWith(PREFIX_ICON); - } - - private static boolean hasCode(final String moreKeySpec) { - final int end = indexOfLabelEnd(moreKeySpec, 0); - if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith( - PREFIX_CODE, end + 1)) { + // This is a workaround to have a key that has a supplementary code point. We can't put a + // string in resource as a XML entity of a supplementary code point or a surrogate pair. + if (keySpec.startsWith(PREFIX_HEX, labelEnd + 1)) { return true; } return false; @@ -151,17 +91,21 @@ public final class KeySpecParser { return sb.toString(); } - private static int indexOfLabelEnd(final String moreKeySpec, final int start) { - if (moreKeySpec.indexOf(BACKSLASH, start) < 0) { - final int end = moreKeySpec.indexOf(VERTICAL_BAR, start); - if (end == 0) { - throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + moreKeySpec); + private static int indexOfLabelEnd(final String keySpec) { + final int length = keySpec.length(); + if (keySpec.indexOf(BACKSLASH) < 0) { + final int labelEnd = keySpec.indexOf(VERTICAL_BAR); + if (labelEnd == 0) { + if (length == 1) { + // Treat a sole vertical bar as a special case of key label. + return -1; + } + throw new KeySpecParserError("Empty label"); } - return end; + return labelEnd; } - final int length = moreKeySpec.length(); - for (int pos = start; pos < length; pos++) { - final char c = moreKeySpec.charAt(pos); + for (int pos = 0; pos < length; pos++) { + final char c = keySpec.charAt(pos); if (c == BACKSLASH && pos + 1 < length) { // Skip escape char pos++; @@ -172,63 +116,85 @@ public final class KeySpecParser { return -1; } - public static String getLabel(final String moreKeySpec) { - if (hasIcon(moreKeySpec)) { + private static String getBeforeLabelEnd(final String keySpec, final int labelEnd) { + return (labelEnd < 0) ? keySpec : keySpec.substring(0, labelEnd); + } + + private static String getAfterLabelEnd(final String keySpec, final int labelEnd) { + return keySpec.substring(labelEnd + /* VERTICAL_BAR */1); + } + + private static void checkDoubleLabelEnd(final String keySpec, final int labelEnd) { + if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) { + return; + } + throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec); + } + + public static String getLabel(final String keySpec) { + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. + return null; + } + if (hasIcon(keySpec)) { return null; } - final int end = indexOfLabelEnd(moreKeySpec, 0); - final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end)) - : parseEscape(moreKeySpec); - if (TextUtils.isEmpty(label)) { - throw new KeySpecParserError("Empty label: " + moreKeySpec); + final int labelEnd = indexOfLabelEnd(keySpec); + final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd)); + if (label.isEmpty()) { + throw new KeySpecParserError("Empty label: " + keySpec); } return label; } - private static String getOutputTextInternal(final String moreKeySpec) { - final int end = indexOfLabelEnd(moreKeySpec, 0); - if (end <= 0) { + private static String getOutputTextInternal(final String keySpec, final int labelEnd) { + if (labelEnd <= 0) { return null; } - if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { - throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec); - } - return parseEscape(moreKeySpec.substring(end + /* VERTICAL_BAR */1)); + checkDoubleLabelEnd(keySpec, labelEnd); + return parseEscape(getAfterLabelEnd(keySpec, labelEnd)); } - static String getOutputText(final String moreKeySpec) { - if (hasCode(moreKeySpec)) { + public static String getOutputText(final String keySpec) { + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. + return null; + } + final int labelEnd = indexOfLabelEnd(keySpec); + if (hasCode(keySpec, labelEnd)) { return null; } - final String outputText = getOutputTextInternal(moreKeySpec); + final String outputText = getOutputTextInternal(keySpec, labelEnd); if (outputText != null) { if (StringUtils.codePointCount(outputText) == 1) { // If output text is one code point, it should be treated as a code. // See {@link #getCode(Resources, String)}. return null; } - if (!TextUtils.isEmpty(outputText)) { - return outputText; + if (outputText.isEmpty()) { + throw new KeySpecParserError("Empty outputText: " + keySpec); } - throw new KeySpecParserError("Empty outputText: " + moreKeySpec); + return outputText; } - final String label = getLabel(moreKeySpec); + final String label = getLabel(keySpec); if (label == null) { - throw new KeySpecParserError("Empty label: " + moreKeySpec); + throw new KeySpecParserError("Empty label: " + keySpec); } // Code is automatically generated for one letter label. See {@link getCode()}. return (StringUtils.codePointCount(label) == 1) ? null : label; } - static int getCode(final String moreKeySpec, final KeyboardCodesSet codesSet) { - if (hasCode(moreKeySpec)) { - final int end = indexOfLabelEnd(moreKeySpec, 0); - if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { - throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec); - } - return parseCode(moreKeySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED); + public static int getCode(final String keySpec) { + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. + return CODE_UNSPECIFIED; + } + final int labelEnd = indexOfLabelEnd(keySpec); + if (hasCode(keySpec, labelEnd)) { + checkDoubleLabelEnd(keySpec, labelEnd); + return parseCode(getAfterLabelEnd(keySpec, labelEnd), CODE_UNSPECIFIED); } - final String outputText = getOutputTextInternal(moreKeySpec); + final String outputText = getOutputTextInternal(keySpec, labelEnd); if (outputText != null) { // If output text is one code point, it should be treated as a code. // See {@link #getOutputText(String)}. @@ -237,138 +203,41 @@ public final class KeySpecParser { } return CODE_OUTPUT_TEXT; } - final String label = getLabel(moreKeySpec); - // Code is automatically generated for one letter label. - if (StringUtils.codePointCount(label) == 1) { - return label.codePointAt(0); - } - return CODE_OUTPUT_TEXT; - } - - public static int parseCode(final String text, final KeyboardCodesSet codesSet, - final int defCode) { - if (text == null) return defCode; - if (text.startsWith(PREFIX_CODE)) { - return codesSet.getCode(text.substring(PREFIX_CODE.length())); - } else if (text.startsWith(PREFIX_HEX)) { - return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16); - } else { - return Integer.parseInt(text); - } - } - - public static int getIconId(final String moreKeySpec) { - if (moreKeySpec != null && hasIcon(moreKeySpec)) { - final int end = moreKeySpec.indexOf(VERTICAL_BAR, PREFIX_ICON.length()); - final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length()) - : moreKeySpec.substring(PREFIX_ICON.length(), end); - return KeyboardIconsSet.getIconId(name); - } - return KeyboardIconsSet.ICON_UNDEFINED; - } - - private static <T> ArrayList<T> arrayAsList(final T[] array, final int start, final int end) { - if (array == null) { - throw new NullPointerException(); - } - if (start < 0 || start > end || end > array.length) { - throw new IllegalArgumentException(); - } - - final ArrayList<T> list = CollectionUtils.newArrayList(end - start); - for (int i = start; i < end; i++) { - list.add(array[i]); + final String label = getLabel(keySpec); + if (label == null) { + throw new KeySpecParserError("Empty label: " + keySpec); } - return list; + // Code is automatically generated for one letter label. + return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT; } - private static final String[] EMPTY_STRING_ARRAY = new String[0]; - - private static String[] filterOutEmptyString(final String[] array) { - if (array == null) { - return EMPTY_STRING_ARRAY; + public static int parseCode(final String text, final int defaultCode) { + if (text == null) { + return defaultCode; } - ArrayList<String> out = null; - for (int i = 0; i < array.length; i++) { - final String entry = array[i]; - if (TextUtils.isEmpty(entry)) { - if (out == null) { - out = arrayAsList(array, 0, i); - } - } else if (out != null) { - out.add(entry); - } + if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) { + return KeyboardCodesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length())); } - if (out == null) { - return array; + // This is a workaround to have a key that has a supplementary code point. We can't put a + // string in resource as a XML entity of a supplementary code point or a surrogate pair. + if (text.startsWith(PREFIX_HEX)) { + return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16); } - return out.toArray(new String[out.size()]); + return defaultCode; } - public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs, - final String[] additionalMoreKeySpecs) { - final String[] moreKeys = filterOutEmptyString(moreKeySpecs); - final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs); - final int moreKeysCount = moreKeys.length; - final int additionalCount = additionalMoreKeys.length; - ArrayList<String> out = null; - int additionalIndex = 0; - for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) { - final String moreKeySpec = moreKeys[moreKeyIndex]; - if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) { - if (additionalIndex < additionalCount) { - // Replace '%' marker with additional more key specification. - final String additionalMoreKey = additionalMoreKeys[additionalIndex]; - if (out != null) { - out.add(additionalMoreKey); - } else { - moreKeys[moreKeyIndex] = additionalMoreKey; - } - additionalIndex++; - } else { - // Filter out excessive '%' marker. - if (out == null) { - out = arrayAsList(moreKeys, 0, moreKeyIndex); - } - } - } else { - if (out != null) { - out.add(moreKeySpec); - } - } + public static int getIconId(final String keySpec) { + if (keySpec == null) { + // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory. + return KeyboardIconsSet.ICON_UNDEFINED; } - if (additionalCount > 0 && additionalIndex == 0) { - // No '%' marker is found in more keys. - // Insert all additional more keys to the head of more keys. - if (DEBUG && out != null) { - throw new RuntimeException("Internal logic error:" - + " moreKeys=" + Arrays.toString(moreKeys) - + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); - } - out = arrayAsList(additionalMoreKeys, additionalIndex, additionalCount); - for (int i = 0; i < moreKeysCount; i++) { - out.add(moreKeys[i]); - } - } else if (additionalIndex < additionalCount) { - // The number of '%' markers are less than additional more keys. - // Append remained additional more keys to the tail of more keys. - if (DEBUG && out != null) { - throw new RuntimeException("Internal logic error:" - + " moreKeys=" + Arrays.toString(moreKeys) - + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); - } - out = arrayAsList(moreKeys, 0, moreKeysCount); - for (int i = additionalIndex; i < additionalCount; i++) { - out.add(additionalMoreKeys[additionalIndex]); - } - } - if (out == null && moreKeysCount > 0) { - return moreKeys; - } else if (out != null && out.size() > 0) { - return out.toArray(new String[out.size()]); - } else { - return null; + if (!hasIcon(keySpec)) { + return KeyboardIconsSet.ICON_UNDEFINED; } + final int labelEnd = indexOfLabelEnd(keySpec); + final String iconName = getBeforeLabelEnd(keySpec, labelEnd) + .substring(KeyboardIconsSet.PREFIX_ICON.length()); + return KeyboardIconsSet.getIconId(iconName); } @SuppressWarnings("serial") @@ -377,122 +246,4 @@ public final class KeySpecParser { super(message); } } - - public static String resolveTextReference(final String rawText, - final KeyboardTextsSet textsSet) { - int level = 0; - String text = rawText; - StringBuilder sb; - do { - level++; - if (level >= MAX_STRING_REFERENCE_INDIRECTION) { - throw new RuntimeException("too many @string/resource indirection: " + text); - } - - final int prefixLen = PREFIX_TEXT.length(); - final int size = text.length(); - if (size < prefixLen) { - return text; - } - - sb = null; - for (int pos = 0; pos < size; pos++) { - final char c = text.charAt(pos); - if (text.startsWith(PREFIX_TEXT, pos) && textsSet != null) { - 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(textsSet.getText(name)); - pos = end - 1; - } else if (c == BACKSLASH) { - if (sb != null) { - // Append both escape character and escaped character. - sb.append(text.substring(pos, Math.min(pos + 2, size))); - } - pos++; - } else if (sb != null) { - sb.append(c); - } - } - - if (sb != null) { - text = sb.toString(); - } - } while (sb != null); - return text; - } - - private static int searchTextNameEnd(final String text, final int start) { - final int size = text.length(); - for (int pos = start; pos < size; pos++) { - final char c = text.charAt(pos); - // Label name should be consisted of [a-zA-Z_0-9]. - if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) { - continue; - } - return pos; - } - return size; - } - - public static int getIntValue(final String[] moreKeys, final String key, - final int defaultValue) { - if (moreKeys == null) { - return defaultValue; - } - final int keyLen = key.length(); - boolean foundValue = false; - int value = defaultValue; - for (int i = 0; i < moreKeys.length; i++) { - final String moreKeySpec = moreKeys[i]; - if (moreKeySpec == null || !moreKeySpec.startsWith(key)) { - continue; - } - moreKeys[i] = null; - try { - if (!foundValue) { - value = Integer.parseInt(moreKeySpec.substring(keyLen)); - foundValue = true; - } - } catch (NumberFormatException e) { - throw new RuntimeException( - "integer should follow after " + key + ": " + moreKeySpec); - } - } - return value; - } - - public static boolean getBooleanValue(final String[] moreKeys, final String key) { - if (moreKeys == null) { - return false; - } - boolean value = false; - for (int i = 0; i < moreKeys.length; i++) { - final String moreKeySpec = moreKeys[i]; - if (moreKeySpec == null || !moreKeySpec.equals(key)) { - continue; - } - moreKeys[i] = null; - value = true; - } - return value; - } - - public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase, - final Locale locale) { - if (!Constants.isLetterCode(code) || !needsToUpperCase) return code; - final String text = new String(new int[] { code } , 0, 1); - final String casedText = KeySpecParser.toUpperCaseOfStringForLocale( - text, needsToUpperCase, locale); - return StringUtils.codePointCount(casedText) == 1 - ? casedText.codePointAt(0) : CODE_UNSPECIFIED; - } - - public static String toUpperCaseOfStringForLocale(final String text, - final boolean needsToUpperCase, final Locale locale) { - if (text == null || !needsToUpperCase) return text; - return text.toUpperCase(locale); - } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java index e6a674334..7941ddd41 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java @@ -32,15 +32,15 @@ public abstract class KeyStyle { protected String parseString(final TypedArray a, final int index) { if (a.hasValue(index)) { - return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet); + return mTextsSet.resolveTextReference(a.getString(index)); } return null; } protected String[] parseStringArray(final TypedArray a, final int index) { if (a.hasValue(index)) { - final String text = KeySpecParser.resolveTextReference(a.getString(index), mTextsSet); - return KeySpecParser.splitKeySpecs(text); + final String text = mTextsSet.resolveTextReference(a.getString(index)); + return MoreKeySpec.splitKeySpecs(text); } return null; } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java index 05d855e31..700c9b07c 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java @@ -27,6 +27,7 @@ import com.android.inputmethod.latin.utils.XmlParseUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import java.util.Arrays; import java.util.HashMap; public final class KeyStylesSet { @@ -90,7 +91,8 @@ public final class KeyStylesSet { } final Object value = mStyleAttributes.get(index); if (value != null) { - return (String[])value; + final String[] array = (String[])value; + return Arrays.copyOf(array, array.length); } final KeyStyle parentStyle = mStyles.get(mParentStyleName); return parentStyle.getStringArray(a, index); @@ -133,15 +135,12 @@ public final class KeyStylesSet { public void readKeyAttributes(final TypedArray keyAttr) { // TODO: Currently not all Key attributes can be declared as style. - readString(keyAttr, R.styleable.Keyboard_Key_code); readString(keyAttr, R.styleable.Keyboard_Key_altCode); - readString(keyAttr, R.styleable.Keyboard_Key_keyLabel); - readString(keyAttr, R.styleable.Keyboard_Key_keyOutputText); + readString(keyAttr, R.styleable.Keyboard_Key_keySpec); readString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel); readStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys); readStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys); readFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags); - readString(keyAttr, R.styleable.Keyboard_Key_keyIcon); readString(keyAttr, R.styleable.Keyboard_Key_keyIconDisabled); readString(keyAttr, R.styleable.Keyboard_Key_keyIconPreview); readInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java index 8bdad364c..df386fce4 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java @@ -16,7 +16,6 @@ package com.android.inputmethod.keyboard.internal; -import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Typeface; import android.util.SparseIntArray; @@ -38,7 +37,7 @@ public final class KeyVisualAttributes { public final float mHintLabelRatio; public final float mPreviewTextRatio; - public final ColorStateList mTextColorStateList; + public final int mTextColor; public final int mTextInactivatedColor; public final int mTextShadowColor; public final int mHintLetterColor; @@ -47,6 +46,8 @@ public final class KeyVisualAttributes { public final int mShiftedLetterHintActivatedColor; public final int mPreviewTextColor; + public final float mHintLabelVerticalAdjustment; + private static final int[] VISUAL_ATTRIBUTE_IDS = { R.styleable.Keyboard_Key_keyTypeface, R.styleable.Keyboard_Key_keyLetterSize, @@ -65,6 +66,7 @@ public final class KeyVisualAttributes { R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor, R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, R.styleable.Keyboard_Key_keyPreviewTextColor, + R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment, }; private static final SparseIntArray sVisualAttributeIds = new SparseIntArray(); private static final int ATTR_DEFINED = 1; @@ -116,7 +118,7 @@ public final class KeyVisualAttributes { mPreviewTextRatio = ResourceUtils.getFraction(keyAttr, R.styleable.Keyboard_Key_keyPreviewTextRatio); - mTextColorStateList = keyAttr.getColorStateList(R.styleable.Keyboard_Key_keyTextColor); + mTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextColor, 0); mTextInactivatedColor = keyAttr.getColor( R.styleable.Keyboard_Key_keyTextInactivatedColor, 0); mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0); @@ -127,5 +129,8 @@ public final class KeyVisualAttributes { mShiftedLetterHintActivatedColor = keyAttr.getColor( R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0); mPreviewTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyPreviewTextColor, 0); + + mHintLabelVerticalAdjustment = ResourceUtils.getFraction(keyAttr, + R.styleable.Keyboard_Key_keyHintLabelVerticalAdjustment, 0.0f); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java index c1ae65695..dfe0df04c 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java @@ -21,6 +21,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.os.Build; +import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; @@ -33,17 +34,16 @@ import com.android.inputmethod.keyboard.KeyboardId; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.ResourceUtils; -import com.android.inputmethod.latin.utils.RunInLocale; import com.android.inputmethod.latin.utils.StringUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import com.android.inputmethod.latin.utils.XmlParseUtils; +import com.android.inputmethod.latin.utils.XmlParseUtils.ParseException; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.Arrays; -import java.util.Locale; /** * Keyboard Building helper. @@ -276,20 +276,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> { params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0); params.mIconsSet.loadIcons(keyboardAttr); - final String language = params.mId.mLocale.getLanguage(); - params.mCodesSet.setLanguage(language); - params.mTextsSet.setLanguage(language); - final RunInLocale<Void> job = new RunInLocale<Void>() { - @Override - protected Void job(final Resources res) { - params.mTextsSet.loadStringResources(mContext); - return null; - } - }; - // Null means the current system locale. - final Locale locale = SubtypeLocaleUtils.isNoLanguage(params.mId.mSubtype) - ? null : params.mId.mLocale; - job.runInLocale(mResources, locale); + params.mTextsSet.setLocale(params.mId.mLocale, mContext); final int resourceId = keyboardAttr.getResourceId( R.styleable.Keyboard_touchPositionCorrectionData, 0); @@ -456,11 +443,15 @@ public class KeyboardBuilder<KP extends KeyboardParams> { if (Build.VERSION.SDK_INT < supportedMinSdkVersion) { continue; } + final int labelFlags = row.getDefaultKeyLabelFlags(); + final int backgroundType = row.getDefaultBackgroundType(); final int x = (int)row.getKeyX(null); final int y = row.getKeyY(); - final Key key = new Key(mParams, label, null /* hintLabel */, 0 /* iconId */, - code, outputText, x, y, (int)keyWidth, (int)row.getRowHeight(), - row.getDefaultKeyLabelFlags(), row.getDefaultBackgroundType()); + final int width = (int)keyWidth; + final int height = row.getRowHeight(); + final Key key = new Key(label, KeyboardIconsSet.ICON_UNDEFINED, code, outputText, + null /* hintLabel */, labelFlags, backgroundType, x, y, width, height, + mParams.mHorizontalGap, mParams.mVerticalGap); endKey(key); row.advanceXPos(keyWidth); } @@ -477,7 +468,15 @@ public class KeyboardBuilder<KP extends KeyboardParams> { if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY); return; } - final Key key = new Key(mResources, mParams, row, parser); + final TypedArray keyAttr = mResources.obtainAttributes( + Xml.asAttributeSet(parser), R.styleable.Keyboard_Key); + final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser); + final String keySpec = keyStyle.getString(keyAttr, R.styleable.Keyboard_Key_keySpec); + if (TextUtils.isEmpty(keySpec)) { + throw new ParseException("Empty keySpec", parser); + } + final Key key = new Key(keySpec, keyAttr, keyStyle, mParams, row); + keyAttr.recycle(); if (DEBUG) { startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY, (key.isEnabled() ? "" : " disabled"), key, Arrays.toString(key.getMoreKeys())); @@ -493,7 +492,11 @@ public class KeyboardBuilder<KP extends KeyboardParams> { if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER); return; } - final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser); + final TypedArray keyAttr = mResources.obtainAttributes( + Xml.asAttributeSet(parser), R.styleable.Keyboard_Key); + final KeyStyle keyStyle = mParams.mKeyStyles.getKeyStyle(keyAttr, parser); + final Key spacer = new Key.Spacer(keyAttr, keyStyle, mParams, row); + keyAttr.recycle(); if (DEBUG) startEndTag("<%s />", TAG_SPACER); XmlParseUtils.checkEndTag(TAG_SPACER, parser); endKey(spacer); @@ -649,10 +652,9 @@ public class KeyboardBuilder<KP extends KeyboardParams> { R.styleable.Keyboard_Case_passwordInput, id.passwordInput()); final boolean clobberSettingsKeyMatched = matchBoolean(caseAttr, R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey); - final boolean shortcutKeyEnabledMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled); - final boolean shortcutKeyOnSymbolsMatched = matchBoolean(caseAttr, - R.styleable.Keyboard_Case_shortcutKeyOnSymbols, id.mShortcutKeyOnSymbols); + final boolean supportsSwitchingToShortcutImeMatched = matchBoolean(caseAttr, + R.styleable.Keyboard_Case_supportsSwitchingToShortcutIme, + id.mSupportsSwitchingToShortcutIme); final boolean hasShortcutKeyMatched = matchBoolean(caseAttr, R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey); final boolean languageSwitchKeyEnabledMatched = matchBoolean(caseAttr, @@ -671,13 +673,12 @@ public class KeyboardBuilder<KP extends KeyboardParams> { final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched && modeMatched && navigateNextMatched && navigatePreviousMatched && passwordInputMatched && clobberSettingsKeyMatched - && shortcutKeyEnabledMatched && shortcutKeyOnSymbolsMatched - && hasShortcutKeyMatched && languageSwitchKeyEnabledMatched - && isMultiLineMatched && imeActionMatched && localeCodeMatched - && languageCodeMatched && countryCodeMatched; + && supportsSwitchingToShortcutImeMatched && hasShortcutKeyMatched + && languageSwitchKeyEnabledMatched && isMultiLineMatched && imeActionMatched + && localeCodeMatched && languageCodeMatched && countryCodeMatched; 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", TAG_CASE, textAttr(caseAttr.getString( R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"), textAttr(caseAttr.getString( @@ -694,10 +695,9 @@ public class KeyboardBuilder<KP extends KeyboardParams> { "clobberSettingsKey"), booleanAttr(caseAttr, R.styleable.Keyboard_Case_passwordInput, "passwordInput"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_shortcutKeyEnabled, - "shortcutKeyEnabled"), - booleanAttr(caseAttr, R.styleable.Keyboard_Case_shortcutKeyOnSymbols, - "shortcutKeyOnSymbols"), + booleanAttr( + caseAttr, R.styleable.Keyboard_Case_supportsSwitchingToShortcutIme, + "supportsSwitchingToShortcutIme"), booleanAttr(caseAttr, R.styleable.Keyboard_Case_hasShortcutKey, "hasShortcutKey"), booleanAttr(caseAttr, R.styleable.Keyboard_Case_languageSwitchKeyEnabled, diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java index dc815e57d..06da5719b 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java @@ -22,20 +22,18 @@ import com.android.inputmethod.latin.utils.CollectionUtils; import java.util.HashMap; public final class KeyboardCodesSet { - private static final HashMap<String, int[]> sLanguageToCodesMap = CollectionUtils.newHashMap(); - private static final HashMap<String, Integer> sNameToIdMap = CollectionUtils.newHashMap(); + public static final String PREFIX_CODE = "!code/"; - private int[] mCodes = DEFAULT; + private static final HashMap<String, Integer> sNameToIdMap = CollectionUtils.newHashMap(); - public void setLanguage(final String language) { - final int[] codes = sLanguageToCodesMap.get(language); - mCodes = (codes != null) ? codes : DEFAULT; + private KeyboardCodesSet() { + // This utility class is not publicly instantiable. } - public int getCode(final String name) { + public static int getCode(final String name) { Integer id = sNameToIdMap.get(name); if (id == null) throw new RuntimeException("Unknown key code: " + name); - return mCodes[id]; + return DEFAULT[id]; } private static final String[] ID_TO_NAME = { @@ -54,27 +52,10 @@ public final class KeyboardCodesSet { "key_shift_enter", "key_language_switch", "key_emoji", + "key_alpha_from_emoji", "key_unspecified", - "key_left_parenthesis", - "key_right_parenthesis", - "key_less_than", - "key_greater_than", - "key_left_square_bracket", - "key_right_square_bracket", - "key_left_curly_bracket", - "key_right_curly_bracket", }; - private static final int CODE_LEFT_PARENTHESIS = '('; - private static final int CODE_RIGHT_PARENTHESIS = ')'; - private static final int CODE_LESS_THAN_SIGN = '<'; - private static final int CODE_GREATER_THAN_SIGN = '>'; - private static final int CODE_LEFT_SQUARE_BRACKET = '['; - private static final int CODE_RIGHT_SQUARE_BRACKET = ']'; - private static final int CODE_LEFT_CURLY_BRACKET = '{'; - private static final int CODE_RIGHT_CURLY_BRACKET = '}'; - - // This array should be aligned with the array RTL below. private static final int[] DEFAULT = { Constants.CODE_TAB, Constants.CODE_ENTER, @@ -91,68 +72,13 @@ public final class KeyboardCodesSet { Constants.CODE_SHIFT_ENTER, Constants.CODE_LANGUAGE_SWITCH, Constants.CODE_EMOJI, + Constants.CODE_ALPHA_FROM_EMOJI, Constants.CODE_UNSPECIFIED, - CODE_LEFT_PARENTHESIS, - CODE_RIGHT_PARENTHESIS, - CODE_LESS_THAN_SIGN, - CODE_GREATER_THAN_SIGN, - CODE_LEFT_SQUARE_BRACKET, - CODE_RIGHT_SQUARE_BRACKET, - CODE_LEFT_CURLY_BRACKET, - CODE_RIGHT_CURLY_BRACKET, - }; - - private static final int[] RTL = { - DEFAULT[0], - DEFAULT[1], - DEFAULT[2], - DEFAULT[3], - DEFAULT[4], - DEFAULT[5], - DEFAULT[6], - DEFAULT[7], - DEFAULT[8], - DEFAULT[9], - DEFAULT[10], - DEFAULT[11], - DEFAULT[12], - DEFAULT[13], - DEFAULT[14], - DEFAULT[15], - CODE_RIGHT_PARENTHESIS, - CODE_LEFT_PARENTHESIS, - CODE_GREATER_THAN_SIGN, - CODE_LESS_THAN_SIGN, - CODE_RIGHT_SQUARE_BRACKET, - CODE_LEFT_SQUARE_BRACKET, - CODE_RIGHT_CURLY_BRACKET, - CODE_LEFT_CURLY_BRACKET, - }; - - private static final String LANGUAGE_DEFAULT = "DEFAULT"; - private static final String LANGUAGE_ARABIC = "ar"; - private static final String LANGUAGE_PERSIAN = "fa"; - private static final String LANGUAGE_HEBREW = "iw"; - - private static final Object[] LANGUAGE_AND_CODES = { - LANGUAGE_DEFAULT, DEFAULT, - LANGUAGE_ARABIC, RTL, - LANGUAGE_PERSIAN, RTL, - LANGUAGE_HEBREW, RTL, }; static { - if (DEFAULT.length != RTL.length || DEFAULT.length != ID_TO_NAME.length) { - throw new RuntimeException("Internal inconsistency"); - } for (int i = 0; i < ID_TO_NAME.length; i++) { sNameToIdMap.put(ID_TO_NAME[i], i); } - - for (int i = 0; i < LANGUAGE_AND_CODES.length; i += 2) { - final String language = (String)LANGUAGE_AND_CODES[i]; - final int[] codes = (int[])LANGUAGE_AND_CODES[i + 1]; - sLanguageToCodesMap.put(language, codes); - } } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java index 336db186e..6c9b5adc3 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java @@ -30,33 +30,51 @@ import java.util.HashMap; public final class KeyboardIconsSet { private static final String TAG = KeyboardIconsSet.class.getSimpleName(); + public static final String PREFIX_ICON = "!icon/"; public static final int ICON_UNDEFINED = 0; private static final int ATTR_UNDEFINED = 0; + private static final String NAME_UNDEFINED = "undefined"; + public static final String NAME_SHIFT_KEY = "shift_key"; + public static final String NAME_SHIFT_KEY_SHIFTED = "shift_key_shifted"; + public static final String NAME_DELETE_KEY = "delete_key"; + public static final String NAME_SETTINGS_KEY = "settings_key"; + public static final String NAME_SPACE_KEY = "space_key"; + public static final String NAME_SPACE_KEY_FOR_NUMBER_LAYOUT = "space_key_for_number_layout"; + public static final String NAME_ENTER_KEY = "enter_key"; + public static final String NAME_SEARCH_KEY = "search_key"; + public static final String NAME_TAB_KEY = "tab_key"; + public static final String NANE_TAB_KEY_PREVIEW = "tab_key_preview"; + public static final String NAME_SHORTCUT_KEY = "shortcut_key"; + public static final String NAME_SHORTCUT_KEY_DISABLED = "shortcut_key_disabled"; + public static final String NAME_LANGUAGE_SWITCH_KEY = "language_switch_key"; + public static final String NAME_ZWNJ_KEY = "zwnj_key"; + public static final String NAME_ZWJ_KEY = "zwj_key"; + public static final String NAME_EMOJI_KEY = "emoji_key"; + private static final SparseIntArray ATTR_ID_TO_ICON_ID = new SparseIntArray(); // Icon name to icon id map. private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap(); private static final Object[] NAMES_AND_ATTR_IDS = { - "undefined", ATTR_UNDEFINED, - "shift_key", R.styleable.Keyboard_iconShiftKey, - "delete_key", R.styleable.Keyboard_iconDeleteKey, - "settings_key", R.styleable.Keyboard_iconSettingsKey, - "space_key", R.styleable.Keyboard_iconSpaceKey, - "enter_key", R.styleable.Keyboard_iconEnterKey, - "search_key", R.styleable.Keyboard_iconSearchKey, - "tab_key", R.styleable.Keyboard_iconTabKey, - "shortcut_key", R.styleable.Keyboard_iconShortcutKey, - "shortcut_for_label", R.styleable.Keyboard_iconShortcutForLabel, - "space_key_for_number_layout", R.styleable.Keyboard_iconSpaceKeyForNumberLayout, - "shift_key_shifted", R.styleable.Keyboard_iconShiftKeyShifted, - "shortcut_key_disabled", R.styleable.Keyboard_iconShortcutKeyDisabled, - "tab_key_preview", R.styleable.Keyboard_iconTabKeyPreview, - "language_switch_key", R.styleable.Keyboard_iconLanguageSwitchKey, - "zwnj_key", R.styleable.Keyboard_iconZwnjKey, - "zwj_key", R.styleable.Keyboard_iconZwjKey, - "emoji_key", R.styleable.Keyboard_iconEmojiKey, + NAME_UNDEFINED, ATTR_UNDEFINED, + NAME_SHIFT_KEY, R.styleable.Keyboard_iconShiftKey, + NAME_DELETE_KEY, R.styleable.Keyboard_iconDeleteKey, + NAME_SETTINGS_KEY, R.styleable.Keyboard_iconSettingsKey, + NAME_SPACE_KEY, R.styleable.Keyboard_iconSpaceKey, + NAME_ENTER_KEY, R.styleable.Keyboard_iconEnterKey, + NAME_SEARCH_KEY, R.styleable.Keyboard_iconSearchKey, + NAME_TAB_KEY, R.styleable.Keyboard_iconTabKey, + NAME_SHORTCUT_KEY, R.styleable.Keyboard_iconShortcutKey, + NAME_SPACE_KEY_FOR_NUMBER_LAYOUT, R.styleable.Keyboard_iconSpaceKeyForNumberLayout, + NAME_SHIFT_KEY_SHIFTED, R.styleable.Keyboard_iconShiftKeyShifted, + NAME_SHORTCUT_KEY_DISABLED, R.styleable.Keyboard_iconShortcutKeyDisabled, + NANE_TAB_KEY_PREVIEW, R.styleable.Keyboard_iconTabKeyPreview, + NAME_LANGUAGE_SWITCH_KEY, R.styleable.Keyboard_iconLanguageSwitchKey, + NAME_ZWNJ_KEY, R.styleable.Keyboard_iconZwnjKey, + NAME_ZWJ_KEY, R.styleable.Keyboard_iconZwjKey, + NAME_EMOJI_KEY, R.styleable.Keyboard_iconEmojiKey, }; private static int NUM_ICONS = NAMES_AND_ATTR_IDS.length / 2; @@ -102,7 +120,7 @@ public final class KeyboardIconsSet { return isValidIconId(iconId) ? ICON_NAMES[iconId] : "unknown<" + iconId + ">"; } - static int getIconId(final String name) { + public static int getIconId(final String name) { Integer iconId = sNameToIdsMap.get(name); if (iconId != null) { return iconId; diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java index d32bb7581..153391eed 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java @@ -62,7 +62,6 @@ public class KeyboardParams { public final ArrayList<Key> mShiftKeys = CollectionUtils.newArrayList(); public final ArrayList<Key> mAltCodeKeysWhileTyping = CollectionUtils.newArrayList(); public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet(); - public final KeyboardCodesSet mCodesSet = new KeyboardCodesSet(); public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet(); public final KeyStylesSet mKeyStyles = new KeyStylesSet(mTextsSet); diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java index dd98c1703..b98ced97c 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java @@ -27,10 +27,10 @@ import com.android.inputmethod.latin.utils.RecapitalizeStatus; * * This class contains all keyboard state transition logic. * - * The input events are {@link #onLoadKeyboard()}, {@link #onSaveKeyboardState()}, - * {@link #onPressKey(int,boolean,int)}, {@link #onReleaseKey(int,boolean)}, - * {@link #onCodeInput(int,int)}, {@link #onFinishSlidingInput()}, - * {@link #onUpdateShiftState(int,int)}, {@link #onResetKeyboardStateToAlphabet()}. + * 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 #onUpdateShiftState(int,int)}, {@link #onResetKeyboardStateToAlphabet(int,int)}. * * The actions are {@link SwitchActions}'s methods. */ @@ -52,7 +52,8 @@ public final class KeyboardState { /** * Request to call back {@link KeyboardState#onUpdateShiftState(int, int)}. */ - public void requestUpdatingShiftState(); + public void requestUpdatingShiftState(final int currentAutoCapsState, + final int currentRecapitalizeState); public void startDoubleTapShiftKeyTimer(); public boolean isInDoubleTapShiftKeyTimeout(); @@ -117,7 +118,8 @@ public final class KeyboardState { mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; } - public void onLoadKeyboard() { + public void onLoadKeyboard(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_EVENT) { Log.d(TAG, "onLoadKeyboard: " + this); } @@ -127,7 +129,7 @@ public final class KeyboardState { mPrevSymbolsKeyboardWasShifted = false; mShiftKeyState.onRelease(); mSymbolKeyState.onRelease(); - onRestoreKeyboardState(); + onRestoreKeyboardState(currentAutoCapsState, currentRecapitalizeState); } private static final int UNSHIFT = 0; @@ -153,13 +155,14 @@ public final class KeyboardState { } } - private void onRestoreKeyboardState() { + private void onRestoreKeyboardState(final int currentAutoCapsState, + final int currentRecapitalizeState) { final SavedKeyboardState state = mSavedKeyboardState; if (DEBUG_EVENT) { Log.d(TAG, "onRestoreKeyboardState: saved=" + state + " " + this); } if (!state.mIsValid || state.mIsAlphabetMode) { - setAlphabetKeyboard(); + setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState); } else if (state.mIsEmojiMode) { setEmojiKeyboard(); } else { @@ -237,7 +240,8 @@ public final class KeyboardState { mAlphabetShiftState.setShiftLocked(shiftLocked); } - private void toggleAlphabetAndSymbols() { + private void toggleAlphabetAndSymbols(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_ACTION) { Log.d(TAG, "toggleAlphabetAndSymbols: " + this); } @@ -251,7 +255,7 @@ public final class KeyboardState { mPrevSymbolsKeyboardWasShifted = false; } else { mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted; - setAlphabetKeyboard(); + setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState); if (mPrevMainKeyboardWasShiftLocked) { setShiftLocked(true); } @@ -261,14 +265,15 @@ public final class KeyboardState { // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout // when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal(). - private void resetKeyboardStateToAlphabet() { + private void resetKeyboardStateToAlphabet(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_ACTION) { Log.d(TAG, "resetKeyboardStateToAlphabet: " + this); } if (mIsAlphabetMode) return; mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted; - setAlphabetKeyboard(); + setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState); if (mPrevMainKeyboardWasShiftLocked) { setShiftLocked(true); } @@ -283,7 +288,8 @@ public final class KeyboardState { } } - private void setAlphabetKeyboard() { + private void setAlphabetKeyboard(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_ACTION) { Log.d(TAG, "setAlphabetKeyboard"); } @@ -294,7 +300,7 @@ public final class KeyboardState { mIsSymbolShifted = false; mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; mSwitchState = SWITCH_STATE_ALPHA; - mSwitchActions.requestUpdatingShiftState(); + mSwitchActions.requestUpdatingShiftState(currentAutoCapsState, currentRecapitalizeState); } private void setSymbolsKeyboard() { @@ -304,6 +310,7 @@ public final class KeyboardState { mSwitchActions.setSymbolsKeyboard(); mIsAlphabetMode = false; mIsSymbolShifted = false; + mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; // Reset alphabet shift state. mAlphabetShiftState.setShiftLocked(false); mSwitchState = SWITCH_STATE_SYMBOL_BEGIN; @@ -316,6 +323,7 @@ public final class KeyboardState { mSwitchActions.setSymbolsShiftedKeyboard(); mIsAlphabetMode = false; mIsSymbolShifted = true; + mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; // Reset alphabet shift state. mAlphabetShiftState.setShiftLocked(false); mSwitchState = SWITCH_STATE_SYMBOL_BEGIN; @@ -327,16 +335,18 @@ public final class KeyboardState { } mIsAlphabetMode = false; mIsEmojiMode = true; + mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; // Remember caps lock mode and reset alphabet shift state. mPrevMainKeyboardWasShiftLocked = mAlphabetShiftState.isShiftLocked(); mAlphabetShiftState.setShiftLocked(false); mSwitchActions.setEmojiKeyboard(); } - public void onPressKey(final int code, final boolean isSinglePointer, final int autoCaps) { + public void onPressKey(final int code, final boolean isSinglePointer, + final int currentAutoCapsState, final int currentRecapitalizeState) { if (DEBUG_EVENT) { - Log.d(TAG, "onPressKey: code=" + Constants.printableCode(code) - + " single=" + isSinglePointer + " autoCaps=" + autoCaps + " " + this); + Log.d(TAG, "onPressKey: code=" + Constants.printableCode(code) + " single=" + + isSinglePointer + " autoCaps=" + currentAutoCapsState + " " + this); } if (code != Constants.CODE_SHIFT) { // Because the double tap shift key timer is to detect two consecutive shift key press, @@ -348,7 +358,7 @@ public final class KeyboardState { } else if (code == Constants.CODE_CAPSLOCK) { // Nothing to do here. See {@link #onReleaseKey(int,boolean)}. } else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) { - onPressSymbol(); + onPressSymbol(currentAutoCapsState, currentRecapitalizeState); } else { mShiftKeyState.onOtherKeyPressed(); mSymbolKeyState.onOtherKeyPressed(); @@ -360,7 +370,8 @@ public final class KeyboardState { // As for #3, please note that it's required to check even when the auto caps mode is // off because, for example, we may be in the #1 state within the manual temporary // shifted mode. - if (!isSinglePointer && mIsAlphabetMode && autoCaps != TextUtils.CAP_MODE_CHARACTERS) { + if (!isSinglePointer && mIsAlphabetMode + && currentAutoCapsState != TextUtils.CAP_MODE_CHARACTERS) { final boolean needsToResetAutoCaps = mAlphabetShiftState.isAutomaticShifted() || (mAlphabetShiftState.isManualShifted() && mShiftKeyState.isReleasing()); if (needsToResetAutoCaps) { @@ -370,31 +381,34 @@ public final class KeyboardState { } } - public void onReleaseKey(final int code, final boolean withSliding) { + public void onReleaseKey(final int code, final boolean withSliding, + final int currentAutoCapsState, final int currentRecapitalizeState) { if (DEBUG_EVENT) { Log.d(TAG, "onReleaseKey: code=" + Constants.printableCode(code) + " sliding=" + withSliding + " " + this); } if (code == Constants.CODE_SHIFT) { - onReleaseShift(withSliding); + onReleaseShift(withSliding, currentAutoCapsState, currentRecapitalizeState); } else if (code == Constants.CODE_CAPSLOCK) { setShiftLocked(!mAlphabetShiftState.isShiftLocked()); } else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) { - onReleaseSymbol(withSliding); + onReleaseSymbol(withSliding, currentAutoCapsState, currentRecapitalizeState); } } - private void onPressSymbol() { - toggleAlphabetAndSymbols(); + private void onPressSymbol(final int currentAutoCapsState, + final int currentRecapitalizeState) { + toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState); mSymbolKeyState.onPress(); mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL; } - private void onReleaseSymbol(final boolean withSliding) { + private void onReleaseSymbol(final boolean withSliding, final int currentAutoCapsState, + final int currentRecapitalizeState) { if (mSymbolKeyState.isChording()) { // Switch back to the previous keyboard mode if the user chords the mode change key and // another key, then releases the mode change key. - toggleAlphabetAndSymbols(); + toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState); } else if (!withSliding) { // If the mode change key is being released without sliding, we should forget the // previous symbols keyboard shift state and simply switch back to symbols layout @@ -415,11 +429,12 @@ public final class KeyboardState { // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout // when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal(). - public void onResetKeyboardStateToAlphabet() { + public void onResetKeyboardStateToAlphabet(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_EVENT) { Log.d(TAG, "onResetKeyboardStateToAlphabet: " + this); } - resetKeyboardStateToAlphabet(); + resetKeyboardStateToAlphabet(currentAutoCapsState, currentRecapitalizeState); } private void updateShiftStateForRecapitalize(final int recapitalizeMode) { @@ -510,7 +525,8 @@ public final class KeyboardState { } } - private void onReleaseShift(final boolean withSliding) { + private void onReleaseShift(final boolean withSliding, final int currentAutoCapsState, + final int currentRecapitalizeState) { if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) { // We are recapitalizing. We should match the keyboard state to the recapitalize // state in priority. @@ -533,7 +549,8 @@ public final class KeyboardState { // After chording input, automatic shift state may have been changed depending on // what characters were input. mShiftKeyState.onRelease(); - mSwitchActions.requestUpdatingShiftState(); + mSwitchActions.requestUpdatingShiftState(currentAutoCapsState, + currentRecapitalizeState); return; } else if (mAlphabetShiftState.isShiftLockShifted() && withSliding) { // In shift locked state, shift has been pressed and slid out to other key. @@ -570,20 +587,21 @@ public final class KeyboardState { mShiftKeyState.onRelease(); } - public void onFinishSlidingInput() { + public void onFinishSlidingInput(final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_EVENT) { Log.d(TAG, "onFinishSlidingInput: " + this); } // Switch back to the previous keyboard mode if the user cancels sliding input. switch (mSwitchState) { case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL: - toggleAlphabetAndSymbols(); + toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState); break; case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE: toggleShiftInSymbols(); break; case SWITCH_STATE_MOMENTARY_ALPHA_SHIFT: - setAlphabetKeyboard(); + setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState); break; } } @@ -592,10 +610,11 @@ public final class KeyboardState { return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER; } - public void onCodeInput(final int code, final int autoCaps) { + public void onCodeInput(final int code, final int currentAutoCapsState, + final int currentRecapitalizeState) { if (DEBUG_EVENT) { Log.d(TAG, "onCodeInput: code=" + Constants.printableCode(code) - + " autoCaps=" + autoCaps + " " + this); + + " autoCaps=" + currentAutoCapsState + " " + this); } switch (mSwitchState) { @@ -631,7 +650,7 @@ public final class KeyboardState { // Switch back to alpha keyboard mode if user types one or more non-space/enter // characters followed by a space/enter. if (isSpaceOrEnter(code)) { - toggleAlphabetAndSymbols(); + toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState); mPrevSymbolsKeyboardWasShifted = false; } break; @@ -639,9 +658,11 @@ public final class KeyboardState { // If the code is a letter, update keyboard shift state. if (Constants.isLetterCode(code)) { - updateAlphabetShiftState(autoCaps, RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE); + updateAlphabetShiftState(currentAutoCapsState, currentRecapitalizeState); } else if (code == Constants.CODE_EMOJI) { setEmojiKeyboard(); + } else if (code == Constants.CODE_ALPHA_FROM_EMOJI) { + setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState); } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java index c2a01b5e8..0047aa4a1 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java @@ -18,80 +18,126 @@ package com.android.inputmethod.keyboard.internal; import android.content.Context; import android.content.res.Resources; +import android.text.TextUtils; import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.RunInLocale; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import java.util.HashMap; +import java.util.Locale; -/** - * !!!!! DO NOT EDIT THIS FILE !!!!! - * - * This file is generated by tools/make-keyboard-text. The base template file is - * tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl - * - * This file must be updated when any text resources in keyboard layout files have been changed. - * These text resources are referred as "!text/<resource_name>" in keyboard XML definitions, - * and should be defined in - * tools/make-keyboard-text/res/values-<locale>/donottranslate-more-keys.xml - * - * To update this file, please run the following commands. - * $ cd $ANDROID_BUILD_TOP - * $ mmm packages/inputmethods/LatinIME/tools/make-keyboard-text - * $ make-keyboard-text -java packages/inputmethods/LatinIME/java/src - * - * The updated source file will be generated to the following path (this file). - * packages/inputmethods/LatinIME/java/src/com/android/inputmethod/keyboard/internal/ - * KeyboardTextsSet.java - */ public final class KeyboardTextsSet { - // Language to texts map. - private static final HashMap<String, String[]> sLocaleToTextsMap = CollectionUtils.newHashMap(); - private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap(); + public static final String PREFIX_TEXT = "!text/"; + public static final String SWITCH_TO_ALPHA_KEY_LABEL = "keylabel_to_alpha"; - private String[] mTexts; + private static final char BACKSLASH = Constants.CODE_BACKSLASH; + private static final int MAX_STRING_REFERENCE_INDIRECTION = 10; + + private String[] mTextsTable; // Resource name to text map. private HashMap<String, String> mResourceNameToTextsMap = CollectionUtils.newHashMap(); - public void setLanguage(final String language) { - mTexts = sLocaleToTextsMap.get(language); - if (mTexts == null) { - mTexts = LANGUAGE_DEFAULT; - } - } - - public void loadStringResources(final Context context) { + public void setLocale(final Locale locale, final Context context) { + mTextsTable = KeyboardTextsTable.getTextsTable(locale); + final Resources res = context.getResources(); final int referenceId = context.getApplicationInfo().labelRes; - loadStringResourcesInternal(context, RESOURCE_NAMES, referenceId); + 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); } @UsedForTesting - void loadStringResourcesInternal(final Context context, final String[] resourceNames, - final int referenceId) { - final Resources res = context.getResources(); - final String packageName = res.getResourcePackageName(referenceId); + void loadStringResourcesInternal(final Resources res, final String[] resourceNames, + final String resourcePackageName) { for (final String resName : resourceNames) { - final int resId = res.getIdentifier(resName, "string", packageName); + final int resId = res.getIdentifier(resName, "string", resourcePackageName); mResourceNameToTextsMap.put(resName, res.getString(resId)); } } public String getText(final String name) { - String text = mResourceNameToTextsMap.get(name); - if (text != null) { - return text; + final String text = mResourceNameToTextsMap.get(name); + return (text != null) ? text : KeyboardTextsTable.getText(name, mTextsTable); + } + + private static int searchTextNameEnd(final String text, final int start) { + final int size = text.length(); + for (int pos = start; pos < size; pos++) { + final char c = text.charAt(pos); + // Label name should be consisted of [a-zA-Z_0-9]. + if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) { + continue; + } + return pos; } - final Integer id = sNameToIdsMap.get(name); - if (id == null) throw new RuntimeException("Unknown label: " + name); - text = (id < mTexts.length) ? mTexts[id] : null; - return (text == null) ? LANGUAGE_DEFAULT[id] : text; + return size; } - private static final String[] RESOURCE_NAMES = { - // These texts' name should be aligned with the @string/<name> in values/strings.xml. + // TODO: Resolve text reference when creating {@link KeyboardTextsTable} class. + public String resolveTextReference(final String rawText) { + if (TextUtils.isEmpty(rawText)) { + return null; + } + int level = 0; + String text = rawText; + StringBuilder sb; + do { + level++; + if (level >= MAX_STRING_REFERENCE_INDIRECTION) { + throw new RuntimeException("Too many " + PREFIX_TEXT + "name indirection: " + text); + } + + final int prefixLen = PREFIX_TEXT.length(); + final int size = text.length(); + if (size < prefixLen) { + break; + } + + sb = null; + for (int pos = 0; pos < size; pos++) { + final char c = text.charAt(pos); + if (text.startsWith(PREFIX_TEXT, pos)) { + 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; + } else if (c == BACKSLASH) { + if (sb != null) { + // Append both escape character and escaped character. + sb.append(text.substring(pos, Math.min(pos + 2, size))); + } + pos++; + } else if (sb != null) { + sb.append(c); + } + } + + if (sb != null) { + text = sb.toString(); + } + } while (sb != null); + 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_search_key", "label_send_key", "label_next_key", "label_done_key", @@ -100,3422 +146,4 @@ public final class KeyboardTextsSet { "label_pause_key", "label_wait_key", }; - - private static final String[] NAMES = { - /* 0 */ "more_keys_for_a", - /* 1 */ "more_keys_for_e", - /* 2 */ "more_keys_for_i", - /* 3 */ "more_keys_for_o", - /* 4 */ "more_keys_for_u", - /* 5 */ "more_keys_for_s", - /* 6 */ "more_keys_for_n", - /* 7 */ "more_keys_for_c", - /* 8 */ "more_keys_for_y", - /* 9 */ "more_keys_for_d", - /* 10 */ "more_keys_for_r", - /* 11 */ "more_keys_for_t", - /* 12 */ "more_keys_for_z", - /* 13 */ "more_keys_for_k", - /* 14 */ "more_keys_for_l", - /* 15 */ "more_keys_for_g", - /* 16 */ "more_keys_for_v", - /* 17 */ "more_keys_for_h", - /* 18 */ "more_keys_for_j", - /* 19 */ "more_keys_for_w", - /* 20 */ "keylabel_for_nordic_row1_11", - /* 21 */ "keylabel_for_nordic_row2_10", - /* 22 */ "keylabel_for_nordic_row2_11", - /* 23 */ "more_keys_for_nordic_row2_10", - /* 24 */ "more_keys_for_nordic_row2_11", - /* 25 */ "keylabel_for_east_slavic_row1_9", - /* 26 */ "keylabel_for_east_slavic_row1_12", - /* 27 */ "keylabel_for_east_slavic_row2_1", - /* 28 */ "keylabel_for_east_slavic_row2_11", - /* 29 */ "keylabel_for_east_slavic_row3_5", - /* 30 */ "more_keys_for_cyrillic_u", - /* 31 */ "more_keys_for_cyrillic_ka", - /* 32 */ "more_keys_for_cyrillic_en", - /* 33 */ "more_keys_for_cyrillic_ghe", - /* 34 */ "more_keys_for_east_slavic_row2_1", - /* 35 */ "more_keys_for_cyrillic_a", - /* 36 */ "more_keys_for_cyrillic_o", - /* 37 */ "more_keys_for_cyrillic_soft_sign", - /* 38 */ "more_keys_for_east_slavic_row2_11", - /* 39 */ "keylabel_for_south_slavic_row1_6", - /* 40 */ "keylabel_for_south_slavic_row2_11", - /* 41 */ "keylabel_for_south_slavic_row3_1", - /* 42 */ "keylabel_for_south_slavic_row3_8", - /* 43 */ "more_keys_for_cyrillic_ie", - /* 44 */ "more_keys_for_cyrillic_i", - /* 45 */ "label_to_alpha_key", - /* 46 */ "single_quotes", - /* 47 */ "double_quotes", - /* 48 */ "single_angle_quotes", - /* 49 */ "double_angle_quotes", - /* 50 */ "more_keys_for_currency_dollar", - /* 51 */ "keylabel_for_currency", - /* 52 */ "more_keys_for_currency", - /* 53 */ "more_keys_for_punctuation", - /* 54 */ "more_keys_for_star", - /* 55 */ "more_keys_for_bullet", - /* 56 */ "more_keys_for_plus", - /* 57 */ "more_keys_for_left_parenthesis", - /* 58 */ "more_keys_for_right_parenthesis", - /* 59 */ "more_keys_for_less_than", - /* 60 */ "more_keys_for_greater_than", - /* 61 */ "more_keys_for_arabic_diacritics", - /* 62 */ "keyhintlabel_for_arabic_diacritics", - /* 63 */ "keylabel_for_symbols_1", - /* 64 */ "keylabel_for_symbols_2", - /* 65 */ "keylabel_for_symbols_3", - /* 66 */ "keylabel_for_symbols_4", - /* 67 */ "keylabel_for_symbols_5", - /* 68 */ "keylabel_for_symbols_6", - /* 69 */ "keylabel_for_symbols_7", - /* 70 */ "keylabel_for_symbols_8", - /* 71 */ "keylabel_for_symbols_9", - /* 72 */ "keylabel_for_symbols_0", - /* 73 */ "label_to_symbol_key", - /* 74 */ "label_to_symbol_with_microphone_key", - /* 75 */ "additional_more_keys_for_symbols_1", - /* 76 */ "additional_more_keys_for_symbols_2", - /* 77 */ "additional_more_keys_for_symbols_3", - /* 78 */ "additional_more_keys_for_symbols_4", - /* 79 */ "additional_more_keys_for_symbols_5", - /* 80 */ "additional_more_keys_for_symbols_6", - /* 81 */ "additional_more_keys_for_symbols_7", - /* 82 */ "additional_more_keys_for_symbols_8", - /* 83 */ "additional_more_keys_for_symbols_9", - /* 84 */ "additional_more_keys_for_symbols_0", - /* 85 */ "more_keys_for_symbols_1", - /* 86 */ "more_keys_for_symbols_2", - /* 87 */ "more_keys_for_symbols_3", - /* 88 */ "more_keys_for_symbols_4", - /* 89 */ "more_keys_for_symbols_5", - /* 90 */ "more_keys_for_symbols_6", - /* 91 */ "more_keys_for_symbols_7", - /* 92 */ "more_keys_for_symbols_8", - /* 93 */ "more_keys_for_symbols_9", - /* 94 */ "more_keys_for_symbols_0", - /* 95 */ "keylabel_for_comma", - /* 96 */ "more_keys_for_comma", - /* 97 */ "keylabel_for_symbols_question", - /* 98 */ "keylabel_for_symbols_semicolon", - /* 99 */ "keylabel_for_symbols_percent", - /* 100 */ "more_keys_for_symbols_exclamation", - /* 101 */ "more_keys_for_symbols_question", - /* 102 */ "more_keys_for_symbols_semicolon", - /* 103 */ "more_keys_for_symbols_percent", - /* 104 */ "keylabel_for_tablet_comma", - /* 105 */ "keyhintlabel_for_tablet_comma", - /* 106 */ "more_keys_for_tablet_comma", - /* 107 */ "keyhintlabel_for_period", - /* 108 */ "more_keys_for_period", - /* 109 */ "keylabel_for_apostrophe", - /* 110 */ "keyhintlabel_for_apostrophe", - /* 111 */ "more_keys_for_apostrophe", - /* 112 */ "more_keys_for_q", - /* 113 */ "more_keys_for_x", - /* 114 */ "keylabel_for_q", - /* 115 */ "keylabel_for_w", - /* 116 */ "keylabel_for_y", - /* 117 */ "keylabel_for_x", - /* 118 */ "keylabel_for_spanish_row2_10", - /* 119 */ "more_keys_for_am_pm", - /* 120 */ "settings_as_more_key", - /* 121 */ "shortcut_as_more_key", - /* 122 */ "action_next_as_more_key", - /* 123 */ "action_previous_as_more_key", - /* 124 */ "label_to_more_symbol_key", - /* 125 */ "label_to_more_symbol_for_tablet_key", - /* 126 */ "label_tab_key", - /* 127 */ "label_to_phone_numeric_key", - /* 128 */ "label_to_phone_symbols_key", - /* 129 */ "label_time_am", - /* 130 */ "label_time_pm", - /* 131 */ "keylabel_for_popular_domain", - /* 132 */ "more_keys_for_popular_domain", - /* 133 */ "more_keys_for_smiley", - /* 134 */ "single_laqm_raqm", - /* 135 */ "single_laqm_raqm_rtl", - /* 136 */ "single_raqm_laqm", - /* 137 */ "double_laqm_raqm", - /* 138 */ "double_laqm_raqm_rtl", - /* 139 */ "double_raqm_laqm", - /* 140 */ "single_lqm_rqm", - /* 141 */ "single_9qm_lqm", - /* 142 */ "single_9qm_rqm", - /* 143 */ "double_lqm_rqm", - /* 144 */ "double_9qm_lqm", - /* 145 */ "double_9qm_rqm", - /* 146 */ "more_keys_for_single_quote", - /* 147 */ "more_keys_for_double_quote", - /* 148 */ "more_keys_for_tablet_double_quote", - /* 149 */ "emoji_key_as_more_key", - }; - - private static final String EMPTY = ""; - - /* Default texts */ - private static final String[] LANGUAGE_DEFAULT = { - /* 0~ */ - EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - /* ~44 */ - // Label for "switch to alphabetic" key. - /* 45 */ "ABC", - /* 46 */ "!text/single_lqm_rqm", - /* 47 */ "!text/double_lqm_rqm", - /* 48 */ "!text/single_laqm_raqm", - /* 49 */ "!text/double_laqm_raqm", - // U+00A2: "¢" CENT SIGN - // U+00A3: "£" POUND SIGN - // U+20AC: "€" EURO SIGN - // U+00A5: "¥" YEN SIGN - // U+20B1: "₱" PESO SIGN - /* 50 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", - /* 51 */ "$", - /* 52 */ "$,\u00A2,\u20AC,\u00A3,\u00A5,\u20B1", - /* 53 */ "!fixedColumnOrder!8,;,/,(,),#,!,\\,,?,&,\\%,+,\",-,:,',@", - // U+2020: "†" DAGGER - // U+2021: "‡" DOUBLE DAGGER - // U+2605: "★" BLACK STAR - /* 54 */ "\u2020,\u2021,\u2605", - // U+266A: "♪" EIGHTH NOTE - // U+2665: "♥" BLACK HEART SUIT - // U+2660: "♠" BLACK SPADE SUIT - // U+2666: "♦" BLACK DIAMOND SUIT - // U+2663: "♣" BLACK CLUB SUIT - /* 55 */ "\u266A,\u2665,\u2660,\u2666,\u2663", - // U+00B1: "±" PLUS-MINUS SIGN - /* 56 */ "\u00B1", - // The all letters need to be mirrored are found at - // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - /* 57 */ "!fixedColumnOrder!3,<,{,[", - /* 58 */ "!fixedColumnOrder!3,>,},]", - // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK - // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - // U+2264: "≤" LESS-THAN OR EQUAL TO - // U+2265: "≥" GREATER-THAN EQUAL TO - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - /* 59 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB", - /* 60 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB", - /* 61 */ EMPTY, - /* 62 */ EMPTY, - /* 63 */ "1", - /* 64 */ "2", - /* 65 */ "3", - /* 66 */ "4", - /* 67 */ "5", - /* 68 */ "6", - /* 69 */ "7", - /* 70 */ "8", - /* 71 */ "9", - /* 72 */ "0", - // Label for "switch to symbols" key. - /* 73 */ "?123", - // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" - // part because it'll be appended by the code. - /* 74 */ "123", - /* 75~ */ - EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - /* ~84 */ - // U+00B9: "¹" SUPERSCRIPT ONE - // U+00BD: "½" VULGAR FRACTION ONE HALF - // U+2153: "⅓" VULGAR FRACTION ONE THIRD - // U+00BC: "¼" VULGAR FRACTION ONE QUARTER - // U+215B: "⅛" VULGAR FRACTION ONE EIGHTH - /* 85 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B", - // U+00B2: "²" SUPERSCRIPT TWO - // U+2154: "⅔" VULGAR FRACTION TWO THIRDS - /* 86 */ "\u00B2,\u2154", - // U+00B3: "³" SUPERSCRIPT THREE - // U+00BE: "¾" VULGAR FRACTION THREE QUARTERS - // U+215C: "⅜" VULGAR FRACTION THREE EIGHTHS - /* 87 */ "\u00B3,\u00BE,\u215C", - // U+2074: "⁴" SUPERSCRIPT FOUR - /* 88 */ "\u2074", - // U+215D: "⅝" VULGAR FRACTION FIVE EIGHTHS - /* 89 */ "\u215D", - /* 90 */ EMPTY, - // U+215E: "⅞" VULGAR FRACTION SEVEN EIGHTHS - /* 91 */ "\u215E", - /* 92 */ EMPTY, - /* 93 */ EMPTY, - // U+207F: "ⁿ" SUPERSCRIPT LATIN SMALL LETTER N - // U+2205: "∅" EMPTY SET - /* 94 */ "\u207F,\u2205", - /* 95 */ ",", - /* 96 */ EMPTY, - /* 97 */ "?", - /* 98 */ ";", - /* 99 */ "%", - // U+00A1: "¡" INVERTED EXCLAMATION MARK - /* 100 */ "\u00A1", - // U+00BF: "¿" INVERTED QUESTION MARK - /* 101 */ "\u00BF", - /* 102 */ EMPTY, - // U+2030: "‰" PER MILLE SIGN - /* 103 */ "\u2030", - /* 104 */ ",", - /* 105~ */ - EMPTY, EMPTY, EMPTY, - /* ~107 */ - // U+2026: "…" HORIZONTAL ELLIPSIS - /* 108 */ "\u2026", - /* 109 */ "\'", - /* 110 */ "\"", - /* 111 */ "\"", - /* 112 */ EMPTY, - /* 113 */ EMPTY, - /* 114 */ "q", - /* 115 */ "w", - /* 116 */ "y", - /* 117 */ "x", - /* 118 */ EMPTY, - /* 119 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm", - /* 120 */ "!icon/settings_key|!code/key_settings", - /* 121 */ "!icon/shortcut_key|!code/key_shortcut", - /* 122 */ "!hasLabels!,!text/label_next_key|!code/key_action_next", - /* 123 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous", - // Label for "switch to more symbol" modifier key. Must be short to fit on key! - /* 124 */ "= \\ <", - // Label for "switch to more symbol" modifier key on tablets. Must be short to fit on key! - /* 125 */ "~ [ <", - // Label for "Tab" key. Must be short to fit on key! - /* 126 */ "Tab", - // Label for "switch to phone numeric" key. Must be short to fit on key! - /* 127 */ "123", - // Label for "switch to phone symbols" key. Must be short to fit on key! - // U+FF0A: "*" FULLWIDTH ASTERISK - // U+FF03: "#" FULLWIDTH NUMBER SIGN - /* 128 */ "\uFF0A\uFF03", - // Key label for "ante meridiem" - /* 129 */ "AM", - // Key label for "post meridiem" - /* 130 */ "PM", - /* 131 */ ".com", - // popular web domains for the locale - most popular, displayed on the keyboard - /* 132 */ "!hasLabels!,.net,.org,.gov,.edu", - /* 133 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ", - // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK - // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - // The following characters don't need BIDI mirroring. - // U+2018: "‘" LEFT SINGLE QUOTATION MARK - // U+2019: "’" RIGHT SINGLE QUOTATION MARK - // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK - // U+201C: "“" LEFT DOUBLE QUOTATION MARK - // U+201D: "”" RIGHT DOUBLE QUOTATION MARK - // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK - // Abbreviations are: - // laqm: LEFT-POINTING ANGLE QUOTATION MARK - // raqm: RIGHT-POINTING ANGLE QUOTATION MARK - // rtl: Right-To-Left script order - // lqm: LEFT QUOTATION MARK - // rqm: RIGHT QUOTATION MARK - // 9qm: LOW-9 QUOTATION MARK - // The following each quotation mark pair consist of - // <opening quotation mark>, <closing quotation mark> - // and is named after (single|double)_<opening quotation mark>_<closing quotation mark>. - /* 134 */ "\u2039,\u203A", - /* 135 */ "\u2039|\u203A,\u203A|\u2039", - /* 136 */ "\u203A,\u2039", - /* 137 */ "\u00AB,\u00BB", - /* 138 */ "\u00AB|\u00BB,\u00BB|\u00AB", - /* 139 */ "\u00BB,\u00AB", - // The following each quotation mark triplet consists of - // <another quotation mark>, <opening quotation mark>, <closing quotation mark> - // and is named after (single|double)_<opening quotation mark>_<closing quotation mark>. - /* 140 */ "\u201A,\u2018,\u2019", - /* 141 */ "\u2019,\u201A,\u2018", - /* 142 */ "\u2018,\u201A,\u2019", - /* 143 */ "\u201E,\u201C,\u201D", - /* 144 */ "\u201D,\u201E,\u201C", - /* 145 */ "\u201C,\u201E,\u201D", - /* 146 */ "!fixedColumnOrder!5,!text/single_quotes,!text/single_angle_quotes", - /* 147 */ "!fixedColumnOrder!5,!text/double_quotes,!text/double_angle_quotes", - /* 148 */ "!fixedColumnOrder!6,!text/double_quotes,!text/single_quotes,!text/double_angle_quotes,!text/single_angle_quotes", - /* 149 */ "!icon/emoji_key|!code/key_emoji", - }; - - /* Language af: Afrikaans */ - private static final String[] LANGUAGE_af = { - // This is the same as Dutch except more keys of y and demoting vowels with diaeresis. - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E1,\u00E2,\u00E4,\u00E0,\u00E6,\u00E3,\u00E5,\u0101", - // 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 - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 2 */ "\u00ED,\u00EC,\u00EF,\u00EE,\u012F,\u012B,\u0133", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\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 - /* 4 */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B", - /* 5 */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - /* 7 */ null, - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 8 */ "\u00FD,\u0133", - }; - - /* Language ar: Arabic */ - private static final String[] LANGUAGE_ar = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0623: "ا" ARABIC LETTER ALEF - // U+200C: ZERO WIDTH NON-JOINER - // U+0628: "ب" ARABIC LETTER BEH - // U+062C: "پ" ARABIC LETTER PEH - /* 45 */ "\u0623\u200C\u0628\u200C\u062C", - /* 46 */ null, - /* 47 */ null, - /* 48 */ "!text/single_laqm_raqm_rtl", - /* 49 */ "!text/double_laqm_raqm_rtl", - /* 50~ */ - null, null, null, - /* ~52 */ - // U+061F: "؟" ARABIC QUESTION MARK - // U+060C: "،" ARABIC COMMA - // U+061B: "؛" ARABIC SEMICOLON - /* 53 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(|),)|(", - // U+2605: "★" BLACK STAR - // U+066D: "٭" ARABIC FIVE POINTED STAR - /* 54 */ "\u2605,\u066D", - // U+266A: "♪" EIGHTH NOTE - /* 55 */ "\u266A", - /* 56 */ null, - // The all letters need to be mirrored are found at - // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS - // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS - /* 57 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", - /* 58 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", - // U+2264: "≤" LESS-THAN OR EQUAL TO - // U+2265: "≥" GREATER-THAN EQUAL TO - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK - // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - /* 59 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", - /* 60 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", - // U+0655: "ٕ" ARABIC HAMZA BELOW - // U+0654: "ٔ" ARABIC HAMZA ABOVE - // U+0652: "ْ" ARABIC SUKUN - // U+064D: "ٍ" ARABIC KASRATAN - // U+064C: "ٌ" ARABIC DAMMATAN - // U+064B: "ً" ARABIC FATHATAN - // U+0651: "ّ" ARABIC SHADDA - // U+0656: "ٖ" ARABIC SUBSCRIPT ALEF - // U+0670: "ٰ" ARABIC LETTER SUPERSCRIPT ALEF - // U+0653: "ٓ" ARABIC MADDAH ABOVE - // U+0650: "ِ" ARABIC KASRA - // U+064F: "ُ" ARABIC DAMMA - // U+064E: "َ" ARABIC FATHA - // U+0640: "ـ" ARABIC TATWEEL - // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. - // Note: The space character is needed as a preceding letter to draw Arabic diacritics characters correctly. - /* 61 */ "!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", - /* 62 */ "\u0651", - // U+0661: "١" ARABIC-INDIC DIGIT ONE - /* 63 */ "\u0661", - // U+0662: "٢" ARABIC-INDIC DIGIT TWO - /* 64 */ "\u0662", - // U+0663: "٣" ARABIC-INDIC DIGIT THREE - /* 65 */ "\u0663", - // U+0664: "٤" ARABIC-INDIC DIGIT FOUR - /* 66 */ "\u0664", - // U+0665: "٥" ARABIC-INDIC DIGIT FIVE - /* 67 */ "\u0665", - // U+0666: "٦" ARABIC-INDIC DIGIT SIX - /* 68 */ "\u0666", - // U+0667: "٧" ARABIC-INDIC DIGIT SEVEN - /* 69 */ "\u0667", - // U+0668: "٨" ARABIC-INDIC DIGIT EIGHT - /* 70 */ "\u0668", - // U+0669: "٩" ARABIC-INDIC DIGIT NINE - /* 71 */ "\u0669", - // U+0660: "٠" ARABIC-INDIC DIGIT ZERO - /* 72 */ "\u0660", - // Label for "switch to symbols" key. - // U+061F: "؟" ARABIC QUESTION MARK - /* 73 */ "\u0663\u0662\u0661\u061F", - // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" - // part because it'll be appended by the code. - /* 74 */ "\u0663\u0662\u0661", - /* 75 */ "1", - /* 76 */ "2", - /* 77 */ "3", - /* 78 */ "4", - /* 79 */ "5", - /* 80 */ "6", - /* 81 */ "7", - /* 82 */ "8", - /* 83 */ "9", - // U+066B: "٫" ARABIC DECIMAL SEPARATOR - // U+066C: "٬" ARABIC THOUSANDS SEPARATOR - /* 84 */ "0,\u066B,\u066C", - /* 85~ */ - null, null, null, null, null, null, null, null, null, null, - /* ~94 */ - // U+060C: "،" ARABIC COMMA - /* 95 */ "\u060C", - /* 96 */ "\\,", - /* 97 */ "\u061F", - /* 98 */ "\u061B", - // U+066A: "٪" ARABIC PERCENT SIGN - /* 99 */ "\u066A", - /* 100 */ null, - /* 101 */ "?", - /* 102 */ ";", - // U+2030: "‰" PER MILLE SIGN - /* 103 */ "\\%,\u2030", - /* 104~ */ - null, null, null, null, null, - /* ~108 */ - // U+060C: "،" ARABIC COMMA - // U+061B: "؛" ARABIC SEMICOLON - // U+061F: "؟" ARABIC QUESTION MARK - /* 109 */ "\u060C", - /* 110 */ "\u061F", - /* 111 */ "\u061F,\u061B,!,:,-,/,\',\"", - }; - - /* Language az: Azerbaijani */ - private static final String[] LANGUAGE_az = { - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - /* 0 */ "\u00E2", - // U+0259: "ə" LATIN SMALL LETTER SCHWA - /* 1 */ "\u0259", - // 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 - /* 2 */ "\u0131,\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", - // 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 - /* 3 */ "\u00F6,\u00F4,\u0153,\u00F2,\u00F3,\u00F5,\u00F8,\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 - /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", - // 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 - /* 5 */ "\u015F,\u00DF,\u015B,\u0161", - /* 6 */ 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 - /* 7 */ "\u00E7,\u0107,\u010D", - /* 8~ */ - null, null, null, null, null, null, null, - /* ~14 */ - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u011F", - }; - - /* Language be: Belarusian */ - private static final String[] LANGUAGE_be = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, - /* ~24 */ - // U+045E: "ў" CYRILLIC SMALL LETTER SHORT U - /* 25 */ "\u045E", - // U+0451: "ё" CYRILLIC SMALL LETTER IO - /* 26 */ "\u0451", - // U+044B: "ы" CYRILLIC SMALL LETTER YERU - /* 27 */ "\u044B", - // U+044D: "э" CYRILLIC SMALL LETTER E - /* 28 */ "\u044D", - // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I - /* 29 */ "\u0456", - /* 30~ */ - null, null, null, null, null, null, null, - /* ~36 */ - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 37 */ "\u044A", - /* 38~ */ - null, null, null, null, null, - /* ~42 */ - // U+0451: "ё" CYRILLIC SMALL LETTER IO - /* 43 */ "\u0451", - /* 44 */ null, - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language bg: Bulgarian */ - private static final String[] LANGUAGE_bg = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ null, - // single_quotes of Bulgarian is default single_quotes_right_left. - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language ca: Catalan */ - private static final String[] LANGUAGE_ca = { - // 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+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E0,\u00E1,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // 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 - /* 1 */ "\u00E8,\u00E9,\u00EB,\u00EA,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", - // 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+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\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 - /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* 5 */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\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 - /* 7 */ "\u00E7,\u0107,\u010D", - /* 8~ */ - null, null, null, null, null, null, - /* ~13 */ - // U+00B7: "·" MIDDLE DOT - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "l\u00B7l,\u0142", - /* 15~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - /* ~52 */ - // U+00B7: "·" MIDDLE DOT - /* 53 */ "!fixedColumnOrder!9,;,/,(,),#,\u00B7,!,\\,,?,&,\\%,+,\",-,:,',@", - /* 54~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, - /* ~107 */ - /* 108 */ "?,\u00B7", - /* 109~ */ - null, null, null, null, null, null, null, null, null, - /* ~117 */ - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - /* 118 */ "\u00E7", - }; - - /* Language cs: Czech */ - private static final String[] LANGUAGE_cs = { - // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // 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 - /* 1 */ "\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 - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EE,\u00EF,\u00EC,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\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 - /* 4 */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B", - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - /* 5 */ "\u0161,\u00DF,\u015B", - // U+0148: "ň" LATIN SMALL LETTER N WITH CARON - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\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 - /* 7 */ "\u010D,\u00E7,\u0107", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u010F", - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - /* 10 */ "\u0159", - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - /* 11 */ "\u0165", - // 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 - /* 12 */ "\u017E,\u017A,\u017C", - /* 13~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language da: Danish */ - private static final String[] LANGUAGE_da = { - // 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 - /* 0 */ "\u00E1,\u00E4,\u00E0,\u00E2,\u00E3,\u0101", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - /* 1 */ "\u00E9,\u00EB", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - /* 2 */ "\u00ED,\u00EF", - // 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 - /* 3 */ "\u00F3,\u00F4,\u00F2,\u00F5,\u0153,\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 - /* 4 */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - /* 5 */ "\u00DF,\u015B,\u0161", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - /* 7 */ null, - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+00F0: "ð" LATIN SMALL LETTER ETH - /* 9 */ "\u00F0", - /* 10~ */ - null, null, null, null, - /* ~13 */ - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u0142", - /* 15~ */ - null, null, null, null, null, - /* ~19 */ - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - /* 20 */ "\u00E5", - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 21 */ "\u00E6", - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 22 */ "\u00F8", - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - /* 23 */ "\u00E4", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - /* 24 */ "\u00F6", - /* 25~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language de: German */ - private static final String[] LANGUAGE_de = { - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E4,\u00E2,\u00E0,\u00E1,\u00E6,\u00E3,\u00E5,\u0101", - // 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 - /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0117", - /* 2 */ null, - // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F6,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u00F8,\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 - /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - /* 5 */ "\u00DF,\u015B,\u0161", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - /* 7~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language el: Greek */ - private static final String[] LANGUAGE_el = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0391: "Α" GREEK CAPITAL LETTER ALPHA - // U+0392: "Β" GREEK CAPITAL LETTER BETA - // U+0393: "Γ" GREEK CAPITAL LETTER GAMMA - /* 45 */ "\u0391\u0392\u0393", - }; - - /* Language en: English */ - private static final String[] LANGUAGE_en = { - // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // 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 - /* 1 */ "\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 - /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC", - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5", - // 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+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - /* 5 */ "\u00DF", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* 6 */ "\u00F1", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - /* 7 */ "\u00E7", - }; - - /* Language eo: Esperanto */ - private static final String[] LANGUAGE_eo = { - // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u0103,\u0105,\u00AA", - // 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 - /* 1 */ "\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 - // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+0131: "ı" LATIN SMALL LETTER DOTLESS I - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 2 */ "\u00ED,\u00EE,\u00EF,\u0129,\u00EC,\u012F,\u012B,\u0131,\u0133", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D,\u0151,\u00BA", - // 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 - // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - // U+00B5: "µ" MICRO SIGN - /* 4 */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B,\u0169,\u0171,\u0173,\u00B5", - // 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 - /* 5 */ "\u00DF,\u0161,\u015B,\u0219,\u015F", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+0148: "ň" LATIN SMALL LETTER N WITH CARON - // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE - // U+014B: "ŋ" LATIN SMALL LETTER ENG - /* 6 */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B", - // 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 - /* 7 */ "\u0107,\u010D,\u00E7,\u010B", - // 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 - /* 8 */ "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 - /* 9 */ "\u00F0,\u010F,\u0111", - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - /* 10 */ "\u0159,\u0155,\u0157", - // 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 - // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE - /* 11 */ "\u0165,\u021B,\u0163,\u0167", - // 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 - /* 12 */ "\u017A,\u017C,\u017E", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - // U+0138: "ĸ" LATIN SMALL LETTER KRA - /* 13 */ "\u0137,\u0138", - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u013A,\u013C,\u013E,\u0140,\u0142", - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - /* 15 */ "\u011F,\u0121,\u0123", - // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX - /* 16 */ "w,\u0175", - // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX - // U+0127: "ħ" LATIN SMALL LETTER H WITH STROKE - /* 17 */ "\u0125,\u0127", - /* 18 */ null, - // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX - /* 19 */ "w,\u0175", - /* 20~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, - /* ~111 */ - /* 112 */ "q", - /* 113 */ "x", - // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX - /* 114 */ "\u015D", - // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX - /* 115 */ "\u011D", - // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE - /* 116 */ "\u016D", - // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX - /* 117 */ "\u0109", - // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX - /* 118 */ "\u0135", - }; - - /* Language es: Spanish */ - private static final String[] LANGUAGE_es = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // 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 - /* 1 */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\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 - /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* 5 */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\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 - /* 7 */ "\u00E7,\u0107,\u010D", - /* 8~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~52 */ - // U+00A1: "¡" INVERTED EXCLAMATION MARK - // U+00BF: "¿" INVERTED QUESTION MARK - /* 53 */ "!fixedColumnOrder!4,;,!,\\,,?,:,\u00A1,@,\u00BF", - /* 54~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, - /* ~105 */ - // U+00A1: "¡" INVERTED EXCLAMATION MARK - /* 106 */ "!,\u00A1", - /* 107 */ null, - // U+00BF: "¿" INVERTED QUESTION MARK - /* 108 */ "?,\u00BF", - /* 109 */ "\"", - /* 110 */ "\'", - /* 111 */ "\'", - /* 112~ */ - null, null, null, null, null, null, - /* ~117 */ - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* 118 */ "\u00F1", - }; - - /* Language et: Estonian */ - private static final String[] LANGUAGE_et = { - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // 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+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - /* 0 */ "\u00E4,\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E5,\u00E6,\u0105", - // 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 - /* 1 */ "\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 - // 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+0131: "ı" LATIN SMALL LETTER DOTLESS I - /* 2 */ "\u012B,\u00EC,\u012F,\u00ED,\u00EE,\u00EF,\u0131", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 3 */ "\u00F6,\u00F5,\u00F2,\u00F3,\u00F4,\u0153,\u0151,\u00F8", - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - // 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+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - /* 4 */ "\u00FC,\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u016F,\u0171", - // 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 - /* 5 */ "\u0161,\u00DF,\u015B,\u015F", - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u0146,\u00F1,\u0144,\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 - /* 7 */ "\u010D,\u00E7,\u0107", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u010F", - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - /* 10 */ "\u0157,\u0159,\u0155", - // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - /* 11 */ "\u0163,\u0165", - // 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 - /* 12 */ "\u017E,\u017C,\u017A", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - /* 13 */ "\u0137", - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - /* 14 */ "\u013C,\u0142,\u013A,\u013E", - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u0123,\u011F", - /* 16~ */ - null, null, null, null, - /* ~19 */ - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - /* 20 */ "\u00FC", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - /* 21 */ "\u00F6", - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - /* 22 */ "\u00E4", - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - /* 23 */ "\u00F5", - /* 24~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language fa: Persian */ - private static final String[] LANGUAGE_fa = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0627: "ا" ARABIC LETTER ALEF - // U+200C: ZERO WIDTH NON-JOINER - // U+0628: "ب" ARABIC LETTER BEH - // U+067E: "پ" ARABIC LETTER PEH - /* 45 */ "\u0627\u200C\u0628\u200C\u067E", - /* 46 */ null, - /* 47 */ null, - /* 48 */ "!text/single_laqm_raqm_rtl", - /* 49 */ "!text/double_laqm_raqm_rtl", - /* 50 */ null, - // U+FDFC: "﷼" RIAL SIGN - /* 51 */ "\uFDFC", - /* 52 */ null, - // U+061F: "؟" ARABIC QUESTION MARK - // U+060C: "،" ARABIC COMMA - // U+061B: "؛" ARABIC SEMICOLON - /* 53 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(|),)|(", - // U+2605: "★" BLACK STAR - // U+066D: "٭" ARABIC FIVE POINTED STAR - /* 54 */ "\u2605,\u066D", - // U+266A: "♪" EIGHTH NOTE - /* 55 */ "\u266A", - /* 56 */ null, - // The all letters need to be mirrored are found at - // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS - // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS - /* 57 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", - /* 58 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", - // U+2264: "≤" LESS-THAN OR EQUAL TO - // U+2265: "≥" GREATER-THAN EQUAL TO - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK - // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - /* 59 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>", - /* 60 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<", - // U+0655: "ٕ" ARABIC HAMZA BELOW - // U+0652: "ْ" ARABIC SUKUN - // U+0651: "ّ" ARABIC SHADDA - // U+064C: "ٌ" ARABIC DAMMATAN - // U+064D: "ٍ" ARABIC KASRATAN - // U+064B: "ً" ARABIC FATHATAN - // U+0654: "ٔ" ARABIC HAMZA ABOVE - // U+0656: "ٖ" ARABIC SUBSCRIPT ALEF - // U+0670: "ٰ" ARABIC LETTER SUPERSCRIPT ALEF - // U+0653: "ٓ" ARABIC MADDAH ABOVE - // U+064F: "ُ" ARABIC DAMMA - // U+0650: "ِ" ARABIC KASRA - // U+064E: "َ" ARABIC FATHA - // U+0640: "ـ" ARABIC TATWEEL - // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. - // Note: The space character is needed as a preceding letter to draw Arabic diacritics characters correctly. - /* 61 */ "!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", - /* 62 */ "\u064B", - // U+06F1: "۱" EXTENDED ARABIC-INDIC DIGIT ONE - /* 63 */ "\u06F1", - // U+06F2: "۲" EXTENDED ARABIC-INDIC DIGIT TWO - /* 64 */ "\u06F2", - // U+06F3: "۳" EXTENDED ARABIC-INDIC DIGIT THREE - /* 65 */ "\u06F3", - // U+06F4: "۴" EXTENDED ARABIC-INDIC DIGIT FOUR - /* 66 */ "\u06F4", - // U+06F5: "۵" EXTENDED ARABIC-INDIC DIGIT FIVE - /* 67 */ "\u06F5", - // U+06F6: "۶" EXTENDED ARABIC-INDIC DIGIT SIX - /* 68 */ "\u06F6", - // U+06F7: "۷" EXTENDED ARABIC-INDIC DIGIT SEVEN - /* 69 */ "\u06F7", - // U+06F8: "۸" EXTENDED ARABIC-INDIC DIGIT EIGHT - /* 70 */ "\u06F8", - // U+06F9: "۹" EXTENDED ARABIC-INDIC DIGIT NINE - /* 71 */ "\u06F9", - // U+06F0: "۰" EXTENDED ARABIC-INDIC DIGIT ZERO - /* 72 */ "\u06F0", - // Label for "switch to symbols" key. - // U+061F: "؟" ARABIC QUESTION MARK - /* 73 */ "\u06F3\u06F2\u06F1\u061F", - // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" - // part because it'll be appended by the code. - /* 74 */ "\u06F3\u06F2\u06F1", - /* 75 */ "1", - /* 76 */ "2", - /* 77 */ "3", - /* 78 */ "4", - /* 79 */ "5", - /* 80 */ "6", - /* 81 */ "7", - /* 82 */ "8", - /* 83 */ "9", - // U+066B: "٫" ARABIC DECIMAL SEPARATOR - // U+066C: "٬" ARABIC THOUSANDS SEPARATOR - /* 84 */ "0,\u066B,\u066C", - /* 85~ */ - null, null, null, null, null, null, null, null, null, null, - /* ~94 */ - // U+060C: "،" ARABIC COMMA - /* 95 */ "\u060C", - /* 96 */ "\\,", - /* 97 */ "\u061F", - /* 98 */ "\u061B", - // U+066A: "٪" ARABIC PERCENT SIGN - /* 99 */ "\u066A", - /* 100 */ null, - /* 101 */ "?", - /* 102 */ ";", - // U+2030: "‰" PER MILLE SIGN - /* 103 */ "\\%,\u2030", - // U+060C: "،" ARABIC COMMA - // U+061B: "؛" ARABIC SEMICOLON - // U+061F: "؟" ARABIC QUESTION MARK - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - /* 104 */ "\u060C", - /* 105 */ "!", - /* 106 */ "!,\\,", - /* 107 */ "\u061F", - /* 108 */ "\u061F,?", - /* 109 */ "\u060C", - /* 110 */ "\u061F", - /* 111 */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,/,\u00AB|\u00BB,\u00BB|\u00AB", - }; - - /* Language fi: Finnish */ - private static final String[] LANGUAGE_fi = { - // 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 - /* 0 */ "\u00E6,\u00E0,\u00E1,\u00E2,\u00E3,\u0101", - /* 1 */ null, - /* 2 */ null, - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F8,\u00F4,\u00F2,\u00F3,\u00F5,\u0153,\u014D", - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - /* 4 */ "\u00FC", - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - /* 5 */ "\u0161,\u00DF,\u015B", - /* 6~ */ - null, null, null, null, null, null, - /* ~11 */ - // 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 - /* 12 */ "\u017E,\u017A,\u017C", - /* 13~ */ - null, null, null, null, null, null, null, - /* ~19 */ - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - /* 20 */ "\u00E5", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - /* 21 */ "\u00F6", - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - /* 22 */ "\u00E4", - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 23 */ "\u00F8", - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 24 */ "\u00E6", - }; - - /* Language fr: French */ - private static final String[] LANGUAGE_fr = { - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E0,\u00E2,%,\u00E6,\u00E1,\u00E4,\u00E3,\u00E5,\u0101,\u00AA", - // 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 - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,%,\u0119,\u0117,\u0113", - // 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 - /* 2 */ "\u00EE,%,\u00EF,\u00EC,\u00ED,\u012F,\u012B", - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // 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 - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\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 - /* 4 */ "\u00F9,\u00FB,%,\u00FC,\u00FA,\u016B", - /* 5 */ null, - /* 6 */ 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 - /* 7 */ "\u00E7,\u0107,\u010D", - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "%,\u00FF", - }; - - /* Language hi: Hindi */ - private static final String[] LANGUAGE_hi = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0915: "क" DEVANAGARI LETTER KA - // U+0916: "ख" DEVANAGARI LETTER KHA - // U+0917: "ग" DEVANAGARI LETTER GA - /* 45 */ "\u0915\u0916\u0917", - /* 46~ */ - null, null, null, null, null, - /* ~50 */ - // U+20B9: "₹" INDIAN RUPEE SIGN - /* 51 */ "\u20B9", - /* 52~ */ - null, null, null, null, null, null, null, null, null, null, null, - /* ~62 */ - // U+0967: "१" DEVANAGARI DIGIT ONE - /* 63 */ "\u0967", - // U+0968: "२" DEVANAGARI DIGIT TWO - /* 64 */ "\u0968", - // U+0969: "३" DEVANAGARI DIGIT THREE - /* 65 */ "\u0969", - // U+096A: "४" DEVANAGARI DIGIT FOUR - /* 66 */ "\u096A", - // U+096B: "५" DEVANAGARI DIGIT FIVE - /* 67 */ "\u096B", - // U+096C: "६" DEVANAGARI DIGIT SIX - /* 68 */ "\u096C", - // U+096D: "७" DEVANAGARI DIGIT SEVEN - /* 69 */ "\u096D", - // U+096E: "८" DEVANAGARI DIGIT EIGHT - /* 70 */ "\u096E", - // U+096F: "९" DEVANAGARI DIGIT NINE - /* 71 */ "\u096F", - // U+0966: "०" DEVANAGARI DIGIT ZERO - /* 72 */ "\u0966", - // Label for "switch to symbols" key. - /* 73 */ "?\u0967\u0968\u0969", - // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" - // part because it'll be appended by the code. - /* 74 */ "\u0967\u0968\u0969", - /* 75 */ "1", - /* 76 */ "2", - /* 77 */ "3", - /* 78 */ "4", - /* 79 */ "5", - /* 80 */ "6", - /* 81 */ "7", - /* 82 */ "8", - /* 83 */ "9", - /* 84 */ "0", - }; - - /* Language hr: Croatian */ - private static final String[] LANGUAGE_hr = { - /* 0~ */ - null, null, null, null, null, - /* ~4 */ - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - /* 5 */ "\u0161,\u015B,\u00DF", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\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 - /* 7 */ "\u010D,\u0107,\u00E7", - /* 8 */ null, - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - /* 9 */ "\u0111", - /* 10 */ null, - /* 11 */ null, - // 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 - /* 12 */ "\u017E,\u017A,\u017C", - /* 13~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language hu: Hungarian */ - private static final String[] LANGUAGE_hu = { - // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // 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 - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\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 - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EE,\u00EF,\u00EC,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE 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+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\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 - /* 4 */ "\u00FA,\u00FC,\u0171,\u00FB,\u00F9,\u016B", - /* 5~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language hy: Armenian */ - private static final String[] LANGUAGE_hy = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - /* ~52 */ - // U+058A: "֊" ARMENIAN HYPHEN - // U+055C: "՜" ARMENIAN EXCLAMATION MARK - // U+055D: "՝" ARMENIAN COMMA - // U+055E: "՞" ARMENIAN QUESTION MARK - // U+0559: "ՙ" ARMENIAN MODIFIER LETTER LEFT HALF RING - // U+055A: "՚" ARMENIAN APOSTROPHE - // U+055B: "՛" ARMENIAN EMPHASIS MARK - // U+055F: "՟" ARMENIAN ABBREVIATION MARK - /* 53 */ "!fixedColumnOrder!8,!,?,\\,,.,\u058A,\u055C,\u055D,\u055E,:,;,@,\u0559,\u055A,\u055B,\u055F", - /* 54~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, - /* ~99 */ - // U+055C: "՜" ARMENIAN EXCLAMATION MARK - // U+00A1: "¡" INVERTED EXCLAMATION MARK - /* 100 */ "\u055C,\u00A1", - // U+055E: "՞" ARMENIAN QUESTION MARK - // U+00BF: "¿" INVERTED QUESTION MARK - /* 101 */ "\u055E,\u00BF", - }; - - /* Language is: Icelandic */ - private static final String[] LANGUAGE_is = { - // 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+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 - /* 0 */ "\u00E1,\u00E4,\u00E6,\u00E5,\u00E0,\u00E2,\u00E3,\u0101", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // 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 - /* 1 */ "\u00E9,\u00EB,\u00E8,\u00EA,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EF,\u00EE,\u00EC,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\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 - /* 4 */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", - /* 5~ */ - null, null, null, - /* ~7 */ - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+00F0: "ð" LATIN SMALL LETTER ETH - /* 9 */ "\u00F0", - /* 10 */ null, - // U+00FE: "þ" LATIN SMALL LETTER THORN - /* 11 */ "\u00FE", - /* 12~ */ - null, null, null, null, null, null, null, null, - /* ~19 */ - // U+00F0: "ð" LATIN SMALL LETTER ETH - /* 20 */ "\u00F0", - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 21 */ "\u00E6", - // U+00FE: "þ" LATIN SMALL LETTER THORN - /* 22 */ "\u00FE", - /* 23~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language it: Italian */ - private static final String[] LANGUAGE_it = { - // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u00AA", - // 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+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0119,\u0117,\u0113", - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // 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+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00EC,\u00ED,\u00EE,\u00EF,\u012F,\u012B", - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\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 - /* 4 */ "\u00F9,\u00FA,\u00FB,\u00FC,\u016B", - }; - - /* Language iw: Hebrew */ - private static final String[] LANGUAGE_iw = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+05D0: "א" HEBREW LETTER ALEF - // U+05D1: "ב" HEBREW LETTER BET - // U+05D2: "ג" HEBREW LETTER GIMEL - /* 45 */ "\u05D0\u05D1\u05D2", - // The following characters don't need BIDI mirroring. - // U+2018: "‘" LEFT SINGLE QUOTATION MARK - // U+2019: "’" RIGHT SINGLE QUOTATION MARK - // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK - // U+201C: "“" LEFT DOUBLE QUOTATION MARK - // U+201D: "”" RIGHT DOUBLE QUOTATION MARK - // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK - /* 46 */ "\u2018,\u2019,\u201A", - /* 47 */ "\u201C,\u201D,\u201E", - /* 48 */ "!text/single_laqm_raqm_rtl", - /* 49 */ "!text/double_laqm_raqm_rtl", - /* 50 */ null, - // U+20AA: "₪" NEW SHEQEL SIGN - /* 51 */ "\u20AA", - /* 52 */ null, - /* 53 */ "!fixedColumnOrder!8,;,/,(|),)|(,#,!,\\,,?,&,\\%,+,\",-,:,',@", - // U+2605: "★" BLACK STAR - /* 54 */ "\u2605", - /* 55 */ null, - // U+00B1: "±" PLUS-MINUS SIGN - // U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN - /* 56 */ "\u00B1,\uFB29", - // The all letters need to be mirrored are found at - // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - /* 57 */ "!fixedColumnOrder!3,<|>,{|},[|]", - /* 58 */ "!fixedColumnOrder!3,>|<,}|{,]|[", - // U+2264: "≤" LESS-THAN OR EQUAL TO - // U+2265: "≥" GREATER-THAN EQUAL TO - // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK - // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - /* 59 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", - /* 60 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", - /* 61~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~104 */ - /* 105 */ "!", - /* 106 */ "!", - /* 107 */ "?", - /* 108 */ "?", - }; - - /* Language ka: Georgian */ - private static final String[] LANGUAGE_ka = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+10D0: "ა" GEORGIAN LETTER AN - // U+10D1: "ბ" GEORGIAN LETTER BAN - // U+10D2: "გ" GEORGIAN LETTER GAN - /* 45 */ "\u10D0\u10D1\u10D2", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language kk: Kazakh */ - private static final String[] LANGUAGE_kk = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, - /* ~24 */ - // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA - /* 25 */ "\u0449", - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 26 */ "\u044A", - // U+044B: "ы" CYRILLIC SMALL LETTER YERU - /* 27 */ "\u044B", - // U+044D: "э" CYRILLIC SMALL LETTER E - /* 28 */ "\u044D", - // U+0438: "и" CYRILLIC SMALL LETTER I - /* 29 */ "\u0438", - // U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U - // U+04B1: "ұ" CYRILLIC SMALL LETTER STRAIGHT U WITH STROKE - /* 30 */ "\u04AF,\u04B1", - // U+049B: "қ" CYRILLIC SMALL LETTER KA WITH DESCENDER - /* 31 */ "\u049B", - // U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER - /* 32 */ "\u04A3", - // U+0493: "ғ" CYRILLIC SMALL LETTER GHE WITH STROKE - /* 33 */ "\u0493", - // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I - /* 34 */ "\u0456", - // U+04D9: "ә" CYRILLIC SMALL LETTER SCHWA - /* 35 */ "\u04D9", - // U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O - /* 36 */ "\u04E9", - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 37 */ "\u044A", - // U+04BB: "һ" CYRILLIC SMALL LETTER SHHA - /* 38 */ "\u04BB", - /* 39~ */ - null, null, null, null, - /* ~42 */ - // U+0451: "ё" CYRILLIC SMALL LETTER IO - /* 43 */ "\u0451", - /* 44 */ null, - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - }; - - /* Language km: Khmer */ - private static final String[] LANGUAGE_km = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+1780: "ក" KHMER LETTER KA - // U+1781: "ខ" KHMER LETTER KHA - // U+1782: "គ" KHMER LETTER KO - /* 45 */ "\u1780\u1781\u1782", - /* 46~ */ - null, null, null, null, - /* ~49 */ - // U+17DB: "៛" KHMER CURRENCY SYMBOL RIEL - /* 50 */ "\u17DB,\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", - }; - - /* Language ky: Kirghiz */ - private static final String[] LANGUAGE_ky = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, - /* ~24 */ - // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA - /* 25 */ "\u0449", - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 26 */ "\u044A", - // U+044B: "ы" CYRILLIC SMALL LETTER YERU - /* 27 */ "\u044B", - // U+044D: "э" CYRILLIC SMALL LETTER E - /* 28 */ "\u044D", - // U+0438: "и" CYRILLIC SMALL LETTER I - /* 29 */ "\u0438", - // U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U - /* 30 */ "\u04AF", - /* 31 */ null, - // U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER - /* 32 */ "\u04A3", - /* 33~ */ - null, null, null, - /* ~35 */ - // U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O - /* 36 */ "\u04E9", - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 37 */ "\u044A", - /* 38~ */ - null, null, null, null, null, - /* ~42 */ - // U+0451: "ё" CYRILLIC SMALL LETTER IO - /* 43 */ "\u0451", - /* 44 */ null, - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - }; - - /* Language lo: Lao */ - private static final String[] LANGUAGE_lo = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0E81: "ກ" LAO LETTER KO - // U+0E82: "ຂ" LAO LETTER KHO SUNG - // U+0E84: "ຄ" LAO LETTER KHO TAM - /* 45 */ "\u0E81\u0E82\u0E84", - /* 46~ */ - null, null, null, null, null, - /* ~50 */ - // U+20AD: "₭" KIP SIGN - /* 51 */ "\u20AD", - }; - - /* Language lt: Lithuanian */ - private static final String[] LANGUAGE_lt = { - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // 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+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 0 */ "\u0105,\u00E4,\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E5,\u00E6", - // 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 - /* 1 */ "\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 - // 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+0131: "ı" LATIN SMALL LETTER DOTLESS I - /* 2 */ "\u012F,\u012B,\u00EC,\u00ED,\u00EE,\u00EF,\u0131", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 3 */ "\u00F6,\u00F5,\u00F2,\u00F3,\u00F4,\u0153,\u0151,\u00F8", - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // 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+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - /* 4 */ "\u016B,\u0173,\u00FC,\u016B,\u00F9,\u00FA,\u00FB,\u016F,\u0171", - // 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 - /* 5 */ "\u0161,\u00DF,\u015B,\u015F", - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u0146,\u00F1,\u0144,\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 - /* 7 */ "\u010D,\u00E7,\u0107", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u010F", - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - /* 10 */ "\u0157,\u0159,\u0155", - // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - /* 11 */ "\u0163,\u0165", - // 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 - /* 12 */ "\u017E,\u017C,\u017A", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - /* 13 */ "\u0137", - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - /* 14 */ "\u013C,\u0142,\u013A,\u013E", - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u0123,\u011F", - /* 16~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language lv: Latvian */ - private static final String[] LANGUAGE_lv = { - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - /* 0 */ "\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E4,\u00E5,\u00E6,\u0105", - // 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 - /* 1 */ "\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 - // 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+0131: "ı" LATIN SMALL LETTER DOTLESS I - /* 2 */ "\u012B,\u012F,\u00EC,\u00ED,\u00EE,\u00EF,\u0131", - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 3 */ "\u00F2,\u00F3,\u00F4,\u00F5,\u00F6,\u0153,\u0151,\u00F8", - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - // 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+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - /* 4 */ "\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u00FC,\u016F,\u0171", - // 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 - /* 5 */ "\u0161,\u00DF,\u015B,\u015F", - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u0146,\u00F1,\u0144,\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 - /* 7 */ "\u010D,\u00E7,\u0107", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u010F", - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - /* 10 */ "\u0157,\u0159,\u0155", - // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - /* 11 */ "\u0163,\u0165", - // 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 - /* 12 */ "\u017E,\u017C,\u017A", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - /* 13 */ "\u0137", - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - /* 14 */ "\u013C,\u0142,\u013A,\u013E", - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u0123,\u011F", - /* 16~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language mk: Macedonian */ - private static final String[] LANGUAGE_mk = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, - /* ~38 */ - // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE - /* 39 */ "\u0455", - // U+045C: "ќ" CYRILLIC SMALL LETTER KJE - /* 40 */ "\u045C", - // U+0437: "з" CYRILLIC SMALL LETTER ZE - /* 41 */ "\u0437", - // U+0453: "ѓ" CYRILLIC SMALL LETTER GJE - /* 42 */ "\u0453", - // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE - /* 43 */ "\u0450", - // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE - /* 44 */ "\u045D", - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language mn: Mongolian */ - private static final String[] LANGUAGE_mn = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46~ */ - null, null, null, null, null, - /* ~50 */ - // U+20AE: "₮" TUGRIK SIGN - /* 51 */ "\u20AE", - }; - - /* Language nb: Norwegian Bokmål */ - private static final String[] LANGUAGE_nb = { - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // 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 - /* 0 */ "\u00E0,\u00E4,\u00E1,\u00E2,\u00E3,\u0101", - // 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 - // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113", - /* 2 */ null, - // 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 - /* 3 */ "\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 - /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", - /* 5~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~19 */ - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - /* 20 */ "\u00E5", - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 21 */ "\u00F8", - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 22 */ "\u00E6", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - /* 23 */ "\u00F6", - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - /* 24 */ "\u00E4", - /* 25~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - }; - - /* Language ne: Nepali */ - private static final String[] LANGUAGE_ne = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0915: "क" DEVANAGARI LETTER KA - // U+0916: "ख" DEVANAGARI LETTER KHA - // U+0917: "ग" DEVANAGARI LETTER GA - /* 45 */ "\u0915\u0916\u0917", - /* 46~ */ - null, null, null, null, null, - /* ~50 */ - // U+0930/U+0941/U+002E "रु." NEPALESE RUPEE SIGN - /* 51 */ "\u0930\u0941.", - /* 52~ */ - null, null, null, null, null, null, null, null, null, null, null, - /* ~62 */ - // U+0967: "१" DEVANAGARI DIGIT ONE - /* 63 */ "\u0967", - // U+0968: "२" DEVANAGARI DIGIT TWO - /* 64 */ "\u0968", - // U+0969: "३" DEVANAGARI DIGIT THREE - /* 65 */ "\u0969", - // U+096A: "४" DEVANAGARI DIGIT FOUR - /* 66 */ "\u096A", - // U+096B: "५" DEVANAGARI DIGIT FIVE - /* 67 */ "\u096B", - // U+096C: "६" DEVANAGARI DIGIT SIX - /* 68 */ "\u096C", - // U+096D: "७" DEVANAGARI DIGIT SEVEN - /* 69 */ "\u096D", - // U+096E: "८" DEVANAGARI DIGIT EIGHT - /* 70 */ "\u096E", - // U+096F: "९" DEVANAGARI DIGIT NINE - /* 71 */ "\u096F", - // U+0966: "०" DEVANAGARI DIGIT ZERO - /* 72 */ "\u0966", - // Label for "switch to symbols" key. - /* 73 */ "?\u0967\u0968\u0969", - // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" - // part because it'll be appended by the code. - /* 74 */ "\u0967\u0968\u0969", - /* 75 */ "1", - /* 76 */ "2", - /* 77 */ "3", - /* 78 */ "4", - /* 79 */ "5", - /* 80 */ "6", - /* 81 */ "7", - /* 82 */ "8", - /* 83 */ "9", - /* 84 */ "0", - }; - - /* Language nl: Dutch */ - private static final String[] LANGUAGE_nl = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E1,\u00E4,\u00E2,\u00E0,\u00E6,\u00E3,\u00E5,\u0101", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // 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 - /* 1 */ "\u00E9,\u00EB,\u00EA,\u00E8,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B,\u0133", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\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 - /* 4 */ "\u00FA,\u00FC,\u00FB,\u00F9,\u016B", - /* 5 */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u00F1,\u0144", - /* 7 */ null, - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 8 */ "\u0133", - /* 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, null, null, null, null, - null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - }; - - /* Language pl: Polish */ - private static final String[] LANGUAGE_pl = { - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u0105,\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK - // 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+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE - // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON - /* 1 */ "\u0119,\u00E8,\u00E9,\u00EA,\u00EB,\u0117,\u0113", - /* 2 */ null, - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D", - /* 4 */ null, - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - /* 5 */ "\u015B,\u00DF,\u0161", - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* 6 */ "\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 - /* 7 */ "\u0107,\u00E7,\u010D", - /* 8~ */ - null, null, null, null, - /* ~11 */ - // 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 - /* 12 */ "\u017C,\u017A,\u017E", - /* 13 */ null, - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u0142", - /* 15~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - }; - - /* Language pt: Portuguese */ - private static final String[] LANGUAGE_pt = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E1,\u00E3,\u00E0,\u00E2,\u00E4,\u00E5,\u00E6,\u00AA", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // 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 - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - /* 1 */ "\u00E9,\u00EA,\u00E8,\u0119,\u0117,\u0113,\u00EB", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EE,\u00EC,\u00EF,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\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 - /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* 5 */ null, - /* 6 */ 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 - /* 7 */ "\u00E7,\u010D,\u0107", - }; - - /* Language rm: Raeto-Romance */ - private static final String[] LANGUAGE_rm = { - /* 0~ */ - null, null, null, - /* ~2 */ - // 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+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 3 */ "\u00F2,\u00F3,\u00F6,\u00F4,\u00F5,\u0153,\u00F8", - }; - - /* Language ro: Romanian */ - private static final String[] LANGUAGE_ro = { - // 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 - /* 0 */ "\u00E2,\u00E3,\u0103,\u00E0,\u00E1,\u00E4,\u00E6,\u00E5,\u0101", - /* 1 */ 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 - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", - /* 3 */ null, - /* 4 */ null, - // 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 - /* 5 */ "\u0219,\u00DF,\u015B,\u0161", - /* 6~ */ - null, null, null, null, null, - /* ~10 */ - // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW - /* 11 */ "\u021B", - /* 12~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_rqm", - /* 47 */ "!text/double_9qm_rqm", - }; - - /* Language ru: Russian */ - private static final String[] LANGUAGE_ru = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, - /* ~24 */ - // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA - /* 25 */ "\u0449", - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 26 */ "\u044A", - // U+044B: "ы" CYRILLIC SMALL LETTER YERU - /* 27 */ "\u044B", - // U+044D: "э" CYRILLIC SMALL LETTER E - /* 28 */ "\u044D", - // U+0438: "и" CYRILLIC SMALL LETTER I - /* 29 */ "\u0438", - /* 30~ */ - null, null, null, null, null, null, null, - /* ~36 */ - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 37 */ "\u044A", - /* 38~ */ - null, null, null, null, null, - /* ~42 */ - // U+0451: "ё" CYRILLIC SMALL LETTER IO - /* 43 */ "\u0451", - /* 44 */ null, - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - }; - - /* Language sk: Slovak */ - private static final String[] LANGUAGE_sk = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // 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+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - /* 0 */ "\u00E1,\u00E4,\u0101,\u00E0,\u00E2,\u00E3,\u00E5,\u00E6,\u0105", - // 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 - /* 1 */ "\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 - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+0131: "ı" LATIN SMALL LETTER DOTLESS I - /* 2 */ "\u00ED,\u012B,\u012F,\u00EC,\u00EE,\u00EF,\u0131", - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - /* 3 */ "\u00F4,\u00F3,\u00F6,\u00F2,\u00F5,\u0153,\u0151,\u00F8", - // 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 - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - /* 4 */ "\u00FA,\u016F,\u00FC,\u016B,\u0173,\u00F9,\u00FB,\u0171", - // 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 - /* 5 */ "\u0161,\u00DF,\u015B,\u015F", - // 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 - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\u0148,\u0146,\u00F1,\u0144,\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 - /* 7 */ "\u010D,\u00E7,\u0107", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF", - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u010F", - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - /* 10 */ "\u0155,\u0159,\u0157", - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA - /* 11 */ "\u0165,\u0163", - // 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 - /* 12 */ "\u017E,\u017C,\u017A", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - /* 13 */ "\u0137", - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u013E,\u013A,\u013C,\u0142", - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u0123,\u011F", - /* 16~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language sl: Slovenian */ - private static final String[] LANGUAGE_sl = { - /* 0~ */ - null, null, null, null, null, - /* ~4 */ - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - /* 5 */ "\u0161", - /* 6 */ null, - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - /* 7 */ "\u010D,\u0107", - /* 8 */ null, - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - /* 9 */ "\u0111", - /* 10 */ null, - /* 11 */ null, - // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON - /* 12 */ "\u017E", - /* 13~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, - /* ~45 */ - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language sr: Serbian */ - private static final String[] LANGUAGE_sr = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, - /* ~38 */ - // TODO: Move these to sr-Latn once we can handle IETF language tag with script name specified. - // BEGIN: More keys definitions for Serbian (Latin) - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // <string name="more_keys_for_s">š,ß,ś</string> - // U+010D: "č" LATIN SMALL LETTER C WITH CARON - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE - // <string name="more_keys_for_c">č,ç,ć</string> - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - // <string name="more_keys_for_d">ď</string> - // 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 - // <string name="more_keys_for_z">ž,ź,ż</string> - // END: More keys definitions for Serbian (Latin) - // BEGIN: More keys definitions for Serbian (Cyrillic) - // U+0437: "з" CYRILLIC SMALL LETTER ZE - /* 39 */ "\u0437", - // U+045B: "ћ" CYRILLIC SMALL LETTER TSHE - /* 40 */ "\u045B", - // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE - /* 41 */ "\u0455", - // U+0452: "ђ" CYRILLIC SMALL LETTER DJE - /* 42 */ "\u0452", - // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE - /* 43 */ "\u0450", - // U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE - /* 44 */ "\u045D", - // END: More keys definitions for Serbian (Cyrillic) - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language sv: Swedish */ - private static final String[] LANGUAGE_sv = { - // 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 - /* 0 */ "\u00E1,\u00E0,\u00E2,\u0105,\u00E3", - // 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 - /* 1 */ "\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 - /* 2 */ "\u00ED,\u00EC,\u00EE,\u00EF", - // 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 - /* 3 */ "\u00F3,\u00F2,\u00F4,\u00F5,\u014D", - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE - // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FC,\u00FA,\u00F9,\u00FB,\u016B", - // 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 - /* 5 */ "\u015B,\u0161,\u015F,\u00DF", - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0148: "ň" LATIN SMALL LETTER N WITH CARON - /* 6 */ "\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 - /* 7 */ "\u00E7,\u0107,\u010D", - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS - // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS - /* 8 */ "\u00FD,\u00FF,\u00FC", - // U+00F0: "ð" LATIN SMALL LETTER ETH - // U+010F: "ď" LATIN SMALL LETTER D WITH CARON - /* 9 */ "\u00F0,\u010F", - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - /* 10 */ "\u0159", - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - // U+00FE: "þ" LATIN SMALL LETTER THORN - /* 11 */ "\u0165,\u00FE", - // 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 - /* 12 */ "\u017A,\u017E,\u017C", - /* 13 */ null, - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u0142", - /* 15~ */ - null, null, null, null, null, - /* ~19 */ - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - /* 20 */ "\u00E5", - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - /* 21 */ "\u00F6", - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - /* 22 */ "\u00E4", - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+0153: "œ" LATIN SMALL LIGATURE OE - /* 23 */ "\u00F8,\u0153", - // U+00E6: "æ" LATIN SMALL LETTER AE - /* 24 */ "\u00E6", - /* 25~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, - /* ~47 */ - /* 48 */ "!text/single_raqm_laqm", - /* 49 */ "!text/double_raqm_laqm", - }; - - /* Language sw: Swahili */ - private static final String[] LANGUAGE_sw = { - // This is the same as English except more_keys_for_g. - // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // 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 - /* 1 */ "\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 - /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC", - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5", - // 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+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - /* 5 */ "\u00DF", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* 6 */ "\u00F1", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - /* 7 */ "\u00E7", - /* 8~ */ - null, null, null, null, null, null, null, - /* ~14 */ - /* 15 */ "g\'", - }; - - /* Language th: Thai */ - private static final String[] LANGUAGE_th = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0E01: "ก" THAI CHARACTER KO KAI - // U+0E02: "ข" THAI CHARACTER KHO KHAI - // U+0E04: "ค" THAI CHARACTER KHO KHWAI - /* 45 */ "\u0E01\u0E02\u0E04", - /* 46~ */ - null, null, null, null, null, - /* ~50 */ - // U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT - /* 51 */ "\u0E3F", - }; - - /* Language tl: Tagalog */ - private static final String[] LANGUAGE_tl = { - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", - // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE - // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE - // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS - // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX - // 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 - /* 1 */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", - // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE - // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B", - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\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 - /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B", - /* 5 */ null, - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - /* 6 */ "\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 - /* 7 */ "\u00E7,\u0107,\u010D", - }; - - /* Language tr: Turkish */ - private static final String[] LANGUAGE_tr = { - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - /* 0 */ "\u00E2", - /* 1 */ 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 - /* 2 */ "\u0131,\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", - // 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 - /* 3 */ "\u00F6,\u00F4,\u0153,\u00F2,\u00F3,\u00F5,\u00F8,\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 - /* 4 */ "\u00FC,\u00FB,\u00F9,\u00FA,\u016B", - // 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 - /* 5 */ "\u015F,\u00DF,\u015B,\u0161", - /* 6 */ 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 - /* 7 */ "\u00E7,\u0107,\u010D", - /* 8~ */ - null, null, null, null, null, null, null, - /* ~14 */ - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - /* 15 */ "\u011F", - }; - - /* Language uk: Ukrainian */ - private static final String[] LANGUAGE_uk = { - /* 0~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, - /* ~24 */ - // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA - /* 25 */ "\u0449", - // U+0457: "ї" CYRILLIC SMALL LETTER YI - /* 26 */ "\u0457", - // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I - /* 27 */ "\u0456", - // U+0454: "є" CYRILLIC SMALL LETTER UKRAINIAN IE - /* 28 */ "\u0454", - // U+0438: "и" CYRILLIC SMALL LETTER I - /* 29 */ "\u0438", - /* 30~ */ - null, null, null, - /* ~32 */ - // U+0491: "ґ" CYRILLIC SMALL LETTER GHE WITH UPTURN - /* 33 */ "\u0491", - // U+0457: "ї" CYRILLIC SMALL LETTER YI - /* 34 */ "\u0457", - /* 35 */ null, - /* 36 */ null, - // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN - /* 37 */ "\u044A", - /* 38~ */ - null, null, null, null, null, null, null, - /* ~44 */ - // Label for "switch to alphabetic" key. - // U+0410: "А" CYRILLIC CAPITAL LETTER A - // U+0411: "Б" CYRILLIC CAPITAL LETTER BE - // U+0412: "В" CYRILLIC CAPITAL LETTER VE - /* 45 */ "\u0410\u0411\u0412", - /* 46 */ "!text/single_9qm_lqm", - /* 47 */ "!text/double_9qm_lqm", - /* 48~ */ - null, null, null, - /* ~50 */ - // U+20B4: "₴" HRYVNIA SIGN - /* 51 */ "\u20B4", - }; - - /* Language vi: Vietnamese */ - private static final String[] LANGUAGE_vi = { - // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE - // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE - // U+1EA3: "ả" LATIN SMALL LETTER A WITH HOOK ABOVE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+1EA1: "ạ" LATIN SMALL LETTER A WITH DOT BELOW - // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE - // U+1EB1: "ằ" LATIN SMALL LETTER A WITH BREVE AND GRAVE - // U+1EAF: "ắ" LATIN SMALL LETTER A WITH BREVE AND ACUTE - // U+1EB3: "ẳ" LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE - // U+1EB5: "ẵ" LATIN SMALL LETTER A WITH BREVE AND TILDE - // U+1EB7: "ặ" LATIN SMALL LETTER A WITH BREVE AND DOT BELOW - // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX - // U+1EA7: "ầ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE - // U+1EA5: "ấ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE - // U+1EA9: "ẩ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE - // U+1EAB: "ẫ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE - // U+1EAD: "ậ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW - /* 0 */ "\u00E0,\u00E1,\u1EA3,\u00E3,\u1EA1,\u0103,\u1EB1,\u1EAF,\u1EB3,\u1EB5,\u1EB7,\u00E2,\u1EA7,\u1EA5,\u1EA9,\u1EAB,\u1EAD", - // 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 - /* 1 */ "\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 - /* 2 */ "\u00EC,\u00ED,\u1EC9,\u0129,\u1ECB", - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+1ECF: "ỏ" LATIN SMALL LETTER O WITH HOOK ABOVE - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+1ECD: "ọ" LATIN SMALL LETTER O WITH DOT BELOW - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+1ED3: "ồ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE - // U+1ED1: "ố" LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE - // U+1ED5: "ổ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE - // U+1ED7: "ỗ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE - // U+1ED9: "ộ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW - // U+01A1: "ơ" LATIN SMALL LETTER O WITH HORN - // U+1EDD: "ờ" LATIN SMALL LETTER O WITH HORN AND GRAVE - // U+1EDB: "ớ" LATIN SMALL LETTER O WITH HORN AND ACUTE - // U+1EDF: "ở" LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE - // U+1EE1: "ỡ" LATIN SMALL LETTER O WITH HORN AND TILDE - // U+1EE3: "ợ" LATIN SMALL LETTER O WITH HORN AND DOT BELOW - /* 3 */ "\u00F2,\u00F3,\u1ECF,\u00F5,\u1ECD,\u00F4,\u1ED3,\u1ED1,\u1ED5,\u1ED7,\u1ED9,\u01A1,\u1EDD,\u1EDB,\u1EDF,\u1EE1,\u1EE3", - // 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 - // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE - // U+1EE5: "ụ" LATIN SMALL LETTER U WITH DOT BELOW - // U+01B0: "ư" LATIN SMALL LETTER U WITH HORN - // U+1EEB: "ừ" LATIN SMALL LETTER U WITH HORN AND GRAVE - // U+1EE9: "ứ" LATIN SMALL LETTER U WITH HORN AND ACUTE - // U+1EED: "ử" LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE - // U+1EEF: "ữ" LATIN SMALL LETTER U WITH HORN AND TILDE - // U+1EF1: "ự" LATIN SMALL LETTER U WITH HORN AND DOT BELOW - /* 4 */ "\u00F9,\u00FA,\u1EE7,\u0169,\u1EE5,\u01B0,\u1EEB,\u1EE9,\u1EED,\u1EEF,\u1EF1", - /* 5~ */ - null, null, null, - /* ~7 */ - // U+1EF3: "ỳ" LATIN SMALL LETTER Y WITH GRAVE - // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE - // U+1EF7: "ỷ" LATIN SMALL LETTER Y WITH HOOK ABOVE - // U+1EF9: "ỹ" LATIN SMALL LETTER Y WITH TILDE - // U+1EF5: "ỵ" LATIN SMALL LETTER Y WITH DOT BELOW - /* 8 */ "\u1EF3,\u00FD,\u1EF7,\u1EF9,\u1EF5", - // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE - /* 9 */ "\u0111", - /* 10~ */ - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, - /* ~50 */ - // U+20AB: "₫" DONG SIGN - /* 51 */ "\u20AB", - }; - - /* Language zu: Zulu */ - private static final String[] LANGUAGE_zu = { - // This is the same as English - // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", - // 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 - /* 1 */ "\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 - /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC", - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5", - // 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+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - /* 5 */ "\u00DF", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* 6 */ "\u00F1", - // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA - /* 7 */ "\u00E7", - }; - - /* Language zz: Alphabet */ - private static final String[] LANGUAGE_zz = { - // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS - // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE - // U+00E6: "æ" LATIN SMALL LETTER AE - // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON - // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE - // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK - // U+00AA: "ª" FEMININE ORDINAL INDICATOR - /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E3,\u00E4,\u00E5,\u00E6,\u00E3,\u00E5,\u0101,\u0103,\u0105,\u00AA", - // 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 - /* 1 */ "\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 - // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS - // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE - // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON - // U+012D: "ĭ" LATIN SMALL LETTER I WITH BREVE - // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK - // U+0131: "ı" LATIN SMALL LETTER DOTLESS I - // U+0133: "ij" LATIN SMALL LIGATURE IJ - /* 2 */ "\u00EC,\u00ED,\u00EE,\u00EF,\u0129,\u012B,\u012D,\u012F,\u0131,\u0133", - // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE - // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE - // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX - // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE - // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS - // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE - // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON - // U+014F: "ŏ" LATIN SMALL LETTER O WITH BREVE - // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE - // U+0153: "œ" LATIN SMALL LIGATURE OE - // U+00BA: "º" MASCULINE ORDINAL INDICATOR - /* 3 */ "\u00F2,\u00F3,\u00F4,\u00F5,\u00F6,\u00F8,\u014D,\u014F,\u0151,\u0153,\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+0169: "ũ" LATIN SMALL LETTER U WITH TILDE - // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON - // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE - // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE - // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE - // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK - /* 4 */ "\u00F9,\u00FA,\u00FB,\u00FC,\u0169,\u016B,\u016D,\u016F,\u0171,\u0173", - // U+00DF: "ß" LATIN SMALL LETTER SHARP S - // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE - // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX - // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA - // U+0161: "š" LATIN SMALL LETTER S WITH CARON - // U+017F: "ſ" LATIN SMALL LETTER LONG S - /* 5 */ "\u00DF,\u015B,\u015D,\u015F,\u0161,\u017F", - // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE - // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA - // U+0148: "ň" LATIN SMALL LETTER N WITH CARON - // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE - // U+014B: "ŋ" LATIN SMALL LETTER ENG - /* 6 */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B", - // 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 - /* 7 */ "\u00E7,\u0107,\u0109,\u010B,\u010D", - // 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 - /* 8 */ "\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 - /* 9 */ "\u010F,\u0111,\u00F0", - // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE - // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA - // U+0159: "ř" LATIN SMALL LETTER R WITH CARON - /* 10 */ "\u0155,\u0157,\u0159", - // U+00FE: "þ" LATIN SMALL LETTER THORN - // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA - // U+0165: "ť" LATIN SMALL LETTER T WITH CARON - // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE - /* 11 */ "\u00FE,\u0163,\u0165,\u0167", - // 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 - /* 12 */ "\u017A,\u017C,\u017E", - // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA - // U+0138: "ĸ" LATIN SMALL LETTER KRA - /* 13 */ "\u0137,\u0138", - // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE - // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA - // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON - // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT - // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE - /* 14 */ "\u013A,\u013C,\u013E,\u0140,\u0142", - // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX - // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE - // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE - // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA - /* 15 */ "\u011D,\u011F,\u0121,\u0123", - /* 16 */ null, - // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX - /* 17 */ "\u0125", - // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX - /* 18 */ "\u0135", - // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX - /* 19 */ "\u0175", - }; - - private static final Object[] LANGUAGES_AND_TEXTS = { - "DEFAULT", LANGUAGE_DEFAULT, /* default */ - "af", LANGUAGE_af, /* Afrikaans */ - "ar", LANGUAGE_ar, /* Arabic */ - "az", LANGUAGE_az, /* Azerbaijani */ - "be", LANGUAGE_be, /* Belarusian */ - "bg", LANGUAGE_bg, /* Bulgarian */ - "ca", LANGUAGE_ca, /* Catalan */ - "cs", LANGUAGE_cs, /* Czech */ - "da", LANGUAGE_da, /* Danish */ - "de", LANGUAGE_de, /* German */ - "el", LANGUAGE_el, /* Greek */ - "en", LANGUAGE_en, /* English */ - "eo", LANGUAGE_eo, /* Esperanto */ - "es", LANGUAGE_es, /* Spanish */ - "et", LANGUAGE_et, /* Estonian */ - "fa", LANGUAGE_fa, /* Persian */ - "fi", LANGUAGE_fi, /* Finnish */ - "fr", LANGUAGE_fr, /* French */ - "hi", LANGUAGE_hi, /* Hindi */ - "hr", LANGUAGE_hr, /* Croatian */ - "hu", LANGUAGE_hu, /* Hungarian */ - "hy", LANGUAGE_hy, /* Armenian */ - "is", LANGUAGE_is, /* Icelandic */ - "it", LANGUAGE_it, /* Italian */ - "iw", LANGUAGE_iw, /* Hebrew */ - "ka", LANGUAGE_ka, /* Georgian */ - "kk", LANGUAGE_kk, /* Kazakh */ - "km", LANGUAGE_km, /* Khmer */ - "ky", LANGUAGE_ky, /* Kirghiz */ - "lo", LANGUAGE_lo, /* Lao */ - "lt", LANGUAGE_lt, /* Lithuanian */ - "lv", LANGUAGE_lv, /* Latvian */ - "mk", LANGUAGE_mk, /* Macedonian */ - "mn", LANGUAGE_mn, /* Mongolian */ - "nb", LANGUAGE_nb, /* Norwegian Bokmål */ - "ne", LANGUAGE_ne, /* Nepali */ - "nl", LANGUAGE_nl, /* Dutch */ - "pl", LANGUAGE_pl, /* Polish */ - "pt", LANGUAGE_pt, /* Portuguese */ - "rm", LANGUAGE_rm, /* Raeto-Romance */ - "ro", LANGUAGE_ro, /* Romanian */ - "ru", LANGUAGE_ru, /* Russian */ - "sk", LANGUAGE_sk, /* Slovak */ - "sl", LANGUAGE_sl, /* Slovenian */ - "sr", LANGUAGE_sr, /* Serbian */ - "sv", LANGUAGE_sv, /* Swedish */ - "sw", LANGUAGE_sw, /* Swahili */ - "th", LANGUAGE_th, /* Thai */ - "tl", LANGUAGE_tl, /* Tagalog */ - "tr", LANGUAGE_tr, /* Turkish */ - "uk", LANGUAGE_uk, /* Ukrainian */ - "vi", LANGUAGE_vi, /* Vietnamese */ - "zu", LANGUAGE_zu, /* Zulu */ - "zz", LANGUAGE_zz, /* Alphabet */ - }; - - static { - int id = 0; - for (final String name : NAMES) { - sNameToIdsMap.put(name, id++); - } - - for (int i = 0; i < LANGUAGES_AND_TEXTS.length; i += 2) { - final String language = (String)LANGUAGES_AND_TEXTS[i]; - final String[] texts = (String[])LANGUAGES_AND_TEXTS[i + 1]; - sLocaleToTextsMap.put(language, texts); - } - } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java new file mode 100644 index 000000000..14fa76744 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsTable.java @@ -0,0 +1,3782 @@ +/* + * 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.keyboard.internal; + +import com.android.inputmethod.latin.utils.CollectionUtils; + +import java.util.HashMap; +import java.util.Locale; + +/** + * !!!!! DO NOT EDIT THIS FILE !!!!! + * + * This file is generated by tools/make-keyboard-text. The base template file is + * tools/make-keyboard-text/res/src/com/android/inputmethod/keyboard/internal/ + * KeyboardTextsTable.tmpl + * + * This file must be updated when any text resources in keyboard layout files have been changed. + * These text resources are referred as "!text/<resource_name>" in keyboard XML definitions, + * and should be defined in + * tools/make-keyboard-text/res/values-<locale>/donottranslate-more-keys.xml + * + * To update this file, please run the following commands. + * $ cd $ANDROID_BUILD_TOP + * $ mmm packages/inputmethods/LatinIME/tools/make-keyboard-text + * $ make-keyboard-text -java packages/inputmethods/LatinIME/java + * + * The updated source file will be generated to the following path (this file). + * packages/inputmethods/LatinIME/java/src/com/android/inputmethod/keyboard/internal/ + * KeyboardTextsTable.java + */ +public final class KeyboardTextsTable { + // Name to index map. + private static final HashMap<String, Integer> sNameToIndexesMap = CollectionUtils.newHashMap(); + // Locale to texts table map. + private static final HashMap<String, String[]> sLocaleToTextsTableMap = + CollectionUtils.newHashMap(); + // TODO: Remove this variable after debugging. + // Texts table to locale maps. + private static final HashMap<String[], String> sTextsTableToLocaleMap = + CollectionUtils.newHashMap(); + + public static String getText(final String name, final String[] textsTable) { + final Integer indexObj = sNameToIndexesMap.get(name); + if (indexObj == null) { + throw new RuntimeException("Unknown text name=" + name + " locale=" + + sTextsTableToLocaleMap.get(textsTable)); + } + final int index = indexObj; + final String text = (index < textsTable.length) ? textsTable[index] : null; + if (text != null) { + return text; + } + // Sanity check. + if (index >= 0 && index < TEXTS_DEFAULT.length) { + return TEXTS_DEFAULT[index]; + } + // Throw exception for debugging purpose. + throw new RuntimeException("Illegal index=" + index + " for name=" + name + + " locale=" + sTextsTableToLocaleMap.get(textsTable)); + } + + public static String[] getTextsTable(final Locale locale) { + final String localeKey = locale.toString(); + if (sLocaleToTextsTableMap.containsKey(localeKey)) { + return sLocaleToTextsTableMap.get(localeKey); + } + final String languageKey = locale.getLanguage(); + if (sLocaleToTextsTableMap.containsKey(languageKey)) { + return sLocaleToTextsTableMap.get(languageKey); + } + return TEXTS_DEFAULT; + } + + private static final String[] NAMES = { + // /* index:histogram */ "name", + /* 0:32 */ "morekeys_a", + /* 1:32 */ "morekeys_o", + /* 2:30 */ "morekeys_u", + /* 3:29 */ "morekeys_e", + /* 4:28 */ "morekeys_i", + /* 5:23 */ "morekeys_c", + /* 6:23 */ "double_quotes", + /* 7:22 */ "morekeys_n", + /* 8:22 */ "single_quotes", + /* 9:21 */ "keylabel_to_alpha", + /* 10:20 */ "morekeys_s", + /* 11:14 */ "morekeys_y", + /* 12:13 */ "morekeys_d", + /* 13:12 */ "morekeys_z", + /* 14:10 */ "morekeys_t", + /* 15:10 */ "morekeys_l", + /* 16: 9 */ "morekeys_g", + /* 17: 9 */ "single_angle_quotes", + /* 18: 9 */ "double_angle_quotes", + /* 19: 9 */ "keyspec_currency", + /* 20: 8 */ "morekeys_r", + /* 21: 6 */ "morekeys_k", + /* 22: 6 */ "morekeys_cyrillic_ie", + /* 23: 5 */ "keyspec_nordic_row1_11", + /* 24: 5 */ "keyspec_nordic_row2_10", + /* 25: 5 */ "keyspec_nordic_row2_11", + /* 26: 5 */ "morekeys_nordic_row2_10", + /* 27: 5 */ "keyspec_east_slavic_row1_9", + /* 28: 5 */ "keyspec_east_slavic_row2_2", + /* 29: 5 */ "keyspec_east_slavic_row2_11", + /* 30: 5 */ "keyspec_east_slavic_row3_5", + /* 31: 5 */ "morekeys_cyrillic_soft_sign", + /* 32: 4 */ "morekeys_nordic_row2_11", + /* 33: 4 */ "morekeys_punctuation", + /* 34: 4 */ "keyspec_symbols_1", + /* 35: 4 */ "keyspec_symbols_2", + /* 36: 4 */ "keyspec_symbols_3", + /* 37: 4 */ "keyspec_symbols_4", + /* 38: 4 */ "keyspec_symbols_5", + /* 39: 4 */ "keyspec_symbols_6", + /* 40: 4 */ "keyspec_symbols_7", + /* 41: 4 */ "keyspec_symbols_8", + /* 42: 4 */ "keyspec_symbols_9", + /* 43: 4 */ "keyspec_symbols_0", + /* 44: 4 */ "keylabel_to_symbol", + /* 45: 4 */ "additional_morekeys_symbols_1", + /* 46: 4 */ "additional_morekeys_symbols_2", + /* 47: 4 */ "additional_morekeys_symbols_3", + /* 48: 4 */ "additional_morekeys_symbols_4", + /* 49: 4 */ "additional_morekeys_symbols_5", + /* 50: 4 */ "additional_morekeys_symbols_6", + /* 51: 4 */ "additional_morekeys_symbols_7", + /* 52: 4 */ "additional_morekeys_symbols_8", + /* 53: 4 */ "additional_morekeys_symbols_9", + /* 54: 4 */ "additional_morekeys_symbols_0", + /* 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 */ "morekeys_tablet_comma", + /* 78: 3 */ "keyhintlabel_period", + /* 79: 3 */ "morekeys_tablet_period", + /* 80: 3 */ "morekeys_question", + /* 81: 2 */ "morekeys_h", + /* 82: 2 */ "morekeys_w", + /* 83: 2 */ "morekeys_east_slavic_row2_2", + /* 84: 2 */ "morekeys_cyrillic_u", + /* 85: 2 */ "morekeys_cyrillic_en", + /* 86: 2 */ "morekeys_cyrillic_ghe", + /* 87: 2 */ "morekeys_cyrillic_o", + /* 88: 2 */ "morekeys_cyrillic_i", + /* 89: 2 */ "keyspec_south_slavic_row1_6", + /* 90: 2 */ "keyspec_south_slavic_row2_11", + /* 91: 2 */ "keyspec_south_slavic_row3_1", + /* 92: 2 */ "keyspec_south_slavic_row3_8", + /* 93: 2 */ "morekeys_tablet_punctuation", + /* 94: 2 */ "keyspec_spanish_row2_10", + /* 95: 2 */ "morekeys_bullet", + /* 96: 2 */ "morekeys_left_parenthesis", + /* 97: 2 */ "morekeys_right_parenthesis", + /* 98: 2 */ "morekeys_arabic_diacritics", + /* 99: 2 */ "keyspec_comma", + /* 100: 2 */ "keyhintlabel_tablet_comma", + /* 101: 2 */ "keyspec_period", + /* 102: 2 */ "morekeys_period", + /* 103: 2 */ "keyspec_tablet_period", + /* 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", + /* 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_key", + }; + + private static final String EMPTY = ""; + + /* Default texts */ + private static final String[] TEXTS_DEFAULT = { + /* morekeys_a ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ morekeys_c */ + /* double_quotes */ "!text/double_lqm_rqm", + /* morekeys_n */ EMPTY, + /* single_quotes */ "!text/single_lqm_rqm", + // Label for "switch to alphabetic" key. + /* keylabel_to_alpha */ "ABC", + /* morekeys_s ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_laqm_raqm", + /* double_angle_quotes */ "!text/double_laqm_raqm", + /* keyspec_currency */ "$", + /* morekeys_r ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ morekeys_nordic_row2_11 */ + /* morekeys_punctuation */ "!autoColumnOrder!8,\\,,?,!,#,!text/keyspec_right_parenthesis,!text/keyspec_left_parenthesis,/,;,',@,:,-,\",+,\\%,&", + /* keyspec_symbols_1 */ "1", + /* keyspec_symbols_2 */ "2", + /* keyspec_symbols_3 */ "3", + /* keyspec_symbols_4 */ "4", + /* keyspec_symbols_5 */ "5", + /* keyspec_symbols_6 */ "6", + /* keyspec_symbols_7 */ "7", + /* keyspec_symbols_8 */ "8", + /* keyspec_symbols_9 */ "9", + /* keyspec_symbols_0 */ "0", + // Label for "switch to symbols" key. + /* keylabel_to_symbol */ "?123", + /* additional_morekeys_symbols_1 ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ additional_morekeys_symbols_0 */ + /* keyspec_tablet_comma */ ",", + /* keyspec_swiss_row1_11 ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ morekeys_swiss_row2_11 */ + // U+2020: "†" DAGGER + // U+2021: "‡" DOUBLE DAGGER + // U+2605: "★" BLACK STAR + /* morekeys_star */ "\u2020,\u2021,\u2605", + // The all letters need to be mirrored are found at + // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt + // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK + // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + // U+2264: "≤" LESS-THAN OR EQUAL TO + // U+2265: "≥" GREATER-THAN EQUAL TO + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + /* keyspec_left_parenthesis */ "(", + /* keyspec_right_parenthesis */ ")", + /* keyspec_left_square_bracket */ "[", + /* keyspec_right_square_bracket */ "]", + /* keyspec_left_curly_bracket */ "{", + /* keyspec_right_curly_bracket */ "}", + /* keyspec_less_than */ "<", + /* keyspec_greater_than */ ">", + /* keyspec_less_than_equal */ "\u2264", + /* keyspec_greater_than_equal */ "\u2265", + /* keyspec_left_double_angle_quote */ "\u00AB", + /* keyspec_right_double_angle_quote */ "\u00BB", + /* keyspec_left_single_angle_quote */ "\u2039", + /* keyspec_right_single_angle_quote */ "\u203A", + /* morekeys_tablet_comma */ EMPTY, + /* keyhintlabel_period */ EMPTY, + /* morekeys_tablet_period */ "!text/morekeys_tablet_punctuation", + // U+00BF: "¿" INVERTED QUESTION MARK + /* morekeys_question */ "\u00BF", + /* morekeys_h ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ keyspec_south_slavic_row3_8 */ + /* morekeys_tablet_punctuation */ "!autoColumnOrder!7,\\,,',#,!text/keyspec_right_parenthesis,!text/keyspec_left_parenthesis,/,;,@,:,-,\",+,\\%,&", + // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE + /* keyspec_spanish_row2_10 */ "\u00F1", + // U+266A: "♪" EIGHTH NOTE + // U+2665: "♥" BLACK HEART SUIT + // U+2660: "♠" BLACK SPADE SUIT + // U+2666: "♦" BLACK DIAMOND SUIT + // U+2663: "♣" BLACK CLUB SUIT + /* 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, + // Comma key + /* keyspec_comma */ ",", + /* keyhintlabel_tablet_comma */ EMPTY, + // Period key + /* keyspec_period */ ".", + /* morekeys_period */ "!text/morekeys_punctuation", + /* keyspec_tablet_period */ ".", + /* keyhintlabel_tablet_period */ EMPTY, + /* keyspec_symbols_question */ "?", + /* keyspec_symbols_semicolon */ ";", + /* keyspec_symbols_percent */ "%", + /* morekeys_symbols_semicolon */ EMPTY, + // U+2030: "‰" PER MILLE SIGN + /* morekeys_symbols_percent */ "\u2030", + /* morekeys_v ~ */ + EMPTY, EMPTY, EMPTY, EMPTY, + /* ~ morekeys_x */ + /* keyspec_q */ "q", + /* keyspec_w */ "w", + /* keyspec_y */ "y", + /* keyspec_x */ "x", + /* morekeys_east_slavic_row2_11 ~ */ + EMPTY, EMPTY, EMPTY, + /* ~ morekeys_cyrillic_a */ + // U+00A2: "¢" CENT SIGN + // U+00A3: "£" POUND SIGN + // U+20AC: "€" EURO SIGN + // U+00A5: "¥" YEN SIGN + // U+20B1: "₱" PESO SIGN + /* morekeys_currency_dollar */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", + // U+00B1: "±" PLUS-MINUS SIGN + /* morekeys_plus */ "\u00B1", + /* morekeys_less_than */ "!fixedColumnOrder!3,!text/keyspec_left_single_angle_quote,!text/keyspec_less_than_equal,!text/keyspec_left_double_angle_quote", + /* morekeys_greater_than */ "!fixedColumnOrder!3,!text/keyspec_right_single_angle_quote,!text/keyspec_greater_than_equal,!text/keyspec_right_double_angle_quote", + // U+00A1: "¡" INVERTED EXCLAMATION MARK + /* morekeys_exclamation */ "\u00A1", + /* morekeys_currency */ "$,\u00A2,\u20AC,\u00A3,\u00A5,\u20B1", + // U+00B9: "¹" SUPERSCRIPT ONE + // U+00BD: "½" VULGAR FRACTION ONE HALF + // U+2153: "⅓" VULGAR FRACTION ONE THIRD + // U+00BC: "¼" VULGAR FRACTION ONE QUARTER + // U+215B: "⅛" VULGAR FRACTION ONE EIGHTH + /* morekeys_symbols_1 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B", + // U+00B2: "²" SUPERSCRIPT TWO + // U+2154: "⅔" VULGAR FRACTION TWO THIRDS + /* morekeys_symbols_2 */ "\u00B2,\u2154", + // U+00B3: "³" SUPERSCRIPT THREE + // U+00BE: "¾" VULGAR FRACTION THREE QUARTERS + // U+215C: "⅜" VULGAR FRACTION THREE EIGHTHS + /* morekeys_symbols_3 */ "\u00B3,\u00BE,\u215C", + // U+2074: "⁴" SUPERSCRIPT FOUR + /* morekeys_symbols_4 */ "\u2074", + // U+215D: "⅝" VULGAR FRACTION FIVE EIGHTHS + /* morekeys_symbols_5 */ "\u215D", + /* morekeys_symbols_6 */ EMPTY, + // U+215E: "⅞" VULGAR FRACTION SEVEN EIGHTHS + /* morekeys_symbols_7 */ "\u215E", + /* morekeys_symbols_8 */ EMPTY, + /* morekeys_symbols_9 */ EMPTY, + // U+207F: "ⁿ" SUPERSCRIPT LATIN SMALL LETTER N + // U+2205: "∅" EMPTY SET + /* morekeys_symbols_0 */ "\u207F,\u2205", + /* morekeys_am_pm */ "!fixedColumnOrder!2,!hasLabels!,!text/keylabel_time_am,!text/keylabel_time_pm", + /* keyspec_settings */ "!icon/settings_key|!code/key_settings", + /* keyspec_shortcut */ "!icon/shortcut_key|!code/key_shortcut", + /* keyspec_action_next */ "!hasLabels!,!text/label_next_key|!code/key_action_next", + /* keyspec_action_previous */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous", + // Label for "switch to more symbol" modifier key ("= \ <"). Must be short to fit on key! + /* keylabel_to_more_symbol */ "= \\\\ <", + // Label for "switch to more symbol" modifier key on tablets. Must be short to fit on key! + /* keylabel_tablet_to_more_symbol */ "~ [ <", + // Label for "switch to phone numeric" key. Must be short to fit on key! + /* keylabel_to_phone_numeric */ "123", + // Label for "switch to phone symbols" key. Must be short to fit on key! + // U+FF0A: "*" FULLWIDTH ASTERISK + // U+FF03: "#" FULLWIDTH NUMBER SIGN + /* keylabel_to_phone_symbols */ "\uFF0A\uFF03", + // Key label for "ante meridiem" + /* keylabel_time_am */ "AM", + // Key label for "post meridiem" + /* keylabel_time_pm */ "PM", + /* keyspec_popular_domain */ ".com", + // popular web domains for the locale - most popular, displayed on the keyboard + /* morekeys_popular_domain */ "!hasLabels!,.net,.org,.gov,.edu", + /* keyspecs_left_parenthesis_more_keys */ "!text/keyspec_less_than,!text/keyspec_left_curly_bracket,!text/keyspec_left_square_bracket", + /* keyspecs_right_parenthesis_more_keys */ "!text/keyspec_greater_than,!text/keyspec_right_curly_bracket,!text/keyspec_right_square_bracket", + // The following characters don't need BIDI mirroring. + // U+2018: "‘" LEFT SINGLE QUOTATION MARK + // U+2019: "’" RIGHT SINGLE QUOTATION MARK + // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK + // U+201C: "“" LEFT DOUBLE QUOTATION MARK + // U+201D: "”" RIGHT DOUBLE QUOTATION MARK + // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK + // Abbreviations are: + // laqm: LEFT-POINTING ANGLE QUOTATION MARK + // raqm: RIGHT-POINTING ANGLE QUOTATION MARK + // lqm: LEFT QUOTATION MARK + // rqm: RIGHT QUOTATION MARK + // 9qm: LOW-9 QUOTATION MARK + // The following each quotation mark pair consist of + // <opening quotation mark>, <closing quotation mark> + // and is named after (single|double)_<opening quotation mark>_<closing quotation mark>. + /* single_laqm_raqm */ "!text/keyspec_left_single_angle_quote,!text/keyspec_right_single_angle_quote", + /* single_raqm_laqm */ "!text/keyspec_right_single_angle_quote,!text/keyspec_left_single_angle_quote", + /* double_laqm_raqm */ "!text/keyspec_left_double_angle_quote,!text/keyspec_right_double_angle_quote", + /* double_raqm_laqm */ "!text/keyspec_right_double_angle_quote,!text/keyspec_left_double_angle_quote", + // The following each quotation mark triplet consists of + // <another quotation mark>, <opening quotation mark>, <closing quotation mark> + // and is named after (single|double)_<opening quotation mark>_<closing quotation mark>. + /* single_lqm_rqm */ "\u201A,\u2018,\u2019", + /* single_9qm_lqm */ "\u2019,\u201A,\u2018", + /* single_9qm_rqm */ "\u2018,\u201A,\u2019", + /* single_rqm_9qm */ "\u2018,\u2019,\u201A", + /* double_lqm_rqm */ "\u201E,\u201C,\u201D", + /* double_9qm_lqm */ "\u201D,\u201E,\u201C", + /* double_9qm_rqm */ "\u201C,\u201E,\u201D", + /* double_rqm_9qm */ "\u201C,\u201D,\u201E", + /* morekeys_single_quote */ "!fixedColumnOrder!5,!text/single_quotes,!text/single_angle_quotes", + /* morekeys_double_quote */ "!fixedColumnOrder!5,!text/double_quotes,!text/double_angle_quotes", + /* morekeys_tablet_double_quote */ "!fixedColumnOrder!6,!text/double_quotes,!text/single_quotes,!text/double_angle_quotes,!text/single_angle_quotes", + /* keyspec_emoji_key */ "!icon/emoji_key|!code/key_emoji", + }; + + /* Locale af: Afrikaans */ + private static final String[] TEXTS_af = { + // This is the same as Dutch except more keys of y and demoting vowels with diaeresis. + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E1,\u00E2,\u00E4,\u00E0,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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", + // 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 + // 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+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // 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_s */ + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* morekeys_y */ "\u00FD,\u0133", + }; + + /* Locale ar: Arabic */ + private static final String[] TEXTS_ar = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // Label for "switch to alphabetic" key. + // U+0623: "أ" ARABIC LETTER ALEF WITH HAMZA ABOVE + // U+200C: ZERO WIDTH NON-JOINER + // U+0628: "ب" ARABIC LETTER BEH + // U+062C: "ج" ARABIC LETTER JEEM + /* keylabel_to_alpha */ "\u0623\u200C\u0628\u200C\u062C", + /* morekeys_s ~ */ + 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_punctuation */ + // U+0661: "١" ARABIC-INDIC DIGIT ONE + /* keyspec_symbols_1 */ "\u0661", + // U+0662: "٢" ARABIC-INDIC DIGIT TWO + /* keyspec_symbols_2 */ "\u0662", + // U+0663: "٣" ARABIC-INDIC DIGIT THREE + /* keyspec_symbols_3 */ "\u0663", + // U+0664: "٤" ARABIC-INDIC DIGIT FOUR + /* keyspec_symbols_4 */ "\u0664", + // U+0665: "٥" ARABIC-INDIC DIGIT FIVE + /* keyspec_symbols_5 */ "\u0665", + // U+0666: "٦" ARABIC-INDIC DIGIT SIX + /* keyspec_symbols_6 */ "\u0666", + // U+0667: "٧" ARABIC-INDIC DIGIT SEVEN + /* keyspec_symbols_7 */ "\u0667", + // U+0668: "٨" ARABIC-INDIC DIGIT EIGHT + /* keyspec_symbols_8 */ "\u0668", + // U+0669: "٩" ARABIC-INDIC DIGIT NINE + /* keyspec_symbols_9 */ "\u0669", + // U+0660: "٠" ARABIC-INDIC DIGIT ZERO + /* keyspec_symbols_0 */ "\u0660", + // Label for "switch to symbols" key. + // U+061F: "؟" ARABIC QUESTION MARK + /* keylabel_to_symbol */ "\u0663\u0662\u0661\u061F", + /* additional_morekeys_symbols_1 */ "1", + /* additional_morekeys_symbols_2 */ "2", + /* additional_morekeys_symbols_3 */ "3", + /* additional_morekeys_symbols_4 */ "4", + /* additional_morekeys_symbols_5 */ "5", + /* additional_morekeys_symbols_6 */ "6", + /* additional_morekeys_symbols_7 */ "7", + /* additional_morekeys_symbols_8 */ "8", + /* additional_morekeys_symbols_9 */ "9", + // U+066B: "٫" ARABIC DECIMAL SEPARATOR + // U+066C: "٬" ARABIC THOUSANDS SEPARATOR + /* additional_morekeys_symbols_0 */ "0,\u066B,\u066C", + // 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, + /* ~ morekeys_swiss_row2_11 */ + // U+2605: "★" BLACK STAR + // U+066D: "٭" ARABIC FIVE POINTED STAR + /* morekeys_star */ "\u2605,\u066D", + // U+2264: "≤" LESS-THAN OR EQUAL TO + // U+2265: "≥" GREATER-THAN EQUAL TO + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK + // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + /* keyspec_left_parenthesis */ "(|)", + /* keyspec_right_parenthesis */ ")|(", + /* keyspec_left_square_bracket */ "[|]", + /* keyspec_right_square_bracket */ "]|[", + /* keyspec_left_curly_bracket */ "{|}", + /* keyspec_right_curly_bracket */ "}|{", + /* keyspec_less_than */ "<|>", + /* keyspec_greater_than */ ">|<", + /* keyspec_less_than_equal */ "\u2264|\u2265", + /* keyspec_greater_than_equal */ "\u2265|\u2264", + /* keyspec_left_double_angle_quote */ "\u00AB|\u00BB", + /* keyspec_right_double_angle_quote */ "\u00BB|\u00AB", + /* keyspec_left_single_angle_quote */ "\u2039|\u203A", + /* keyspec_right_single_angle_quote */ "\u203A|\u2039", + /* 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 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ keyspec_spanish_row2_10 */ + // U+266A: "♪" EIGHTH NOTE + /* morekeys_bullet */ "\u266A", + // The all letters need to be mirrored are found at + // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt + // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS + // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS + /* morekeys_left_parenthesis */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,!text/keyspecs_left_parenthesis_more_keys", + /* morekeys_right_parenthesis */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,!text/keyspecs_right_parenthesis_more_keys", + // U+0655: "ٕ" ARABIC HAMZA BELOW + // U+0654: "ٔ" ARABIC HAMZA ABOVE + // U+0652: "ْ" ARABIC SUKUN + // U+064D: "ٍ" ARABIC KASRATAN + // U+064C: "ٌ" ARABIC DAMMATAN + // U+064B: "ً" ARABIC FATHATAN + // U+0651: "ّ" ARABIC SHADDA + // U+0656: "ٖ" ARABIC SUBSCRIPT ALEF + // U+0670: "ٰ" ARABIC LETTER SUPERSCRIPT ALEF + // U+0653: "ٓ" ARABIC MADDAH ABOVE + // U+0650: "ِ" ARABIC KASRA + // U+064F: "ُ" ARABIC DAMMA + // U+064E: "َ" ARABIC FATHA + // U+0640: "ـ" ARABIC TATWEEL + // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. + // 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", + // U+060C: "،" ARABIC COMMA + /* keyspec_comma */ "\u060C", + /* 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", + // U+066A: "٪" ARABIC PERCENT SIGN + /* keyspec_symbols_percent */ "\u066A", + /* morekeys_symbols_semicolon */ ";", + // U+2030: "‰" PER MILLE SIGN + /* morekeys_symbols_percent */ "\\%,\u2030", + }; + + /* Locale az_AZ: Azerbaijani (Azerbaijan) */ + private static final String[] TEXTS_az_AZ = { + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + /* morekeys_a */ "\u00E2", + // 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+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", + // 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 + // 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+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, + /* ~ keylabel_to_alpha */ + // 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", + /* morekeys_y ~ */ + null, null, null, null, null, + /* ~ morekeys_l */ + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u011F", + }; + + /* Locale be_BY: Belarusian (Belarus) */ + private static final String[] TEXTS_be_BY = { + /* morekeys_a ~ */ + null, null, null, null, null, null, + /* ~ morekeys_c */ + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_k */ + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* morekeys_cyrillic_ie */ "\u0451", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, + /* ~ morekeys_nordic_row2_10 */ + // U+045E: "ў" CYRILLIC SMALL LETTER SHORT U + /* keyspec_east_slavic_row1_9 */ "\u045E", + // U+044B: "ы" CYRILLIC SMALL LETTER YERU + /* keyspec_east_slavic_row2_2 */ "\u044B", + // U+044D: "э" CYRILLIC SMALL LETTER E + /* keyspec_east_slavic_row2_11 */ "\u044D", + // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I + /* keyspec_east_slavic_row3_5 */ "\u0456", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* morekeys_cyrillic_soft_sign */ "\u044A", + }; + + /* Locale bg: Bulgarian */ + private static final String[] TEXTS_bg = { + /* morekeys_a ~ */ + null, null, null, null, null, null, + /* ~ morekeys_c */ + // single_quotes of Bulgarian is default single_quotes_right_left. + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ null, + // 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", + }; + + /* Locale ca: Catalan */ + private static final String[] TEXTS_ca = { + // 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+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E0,\u00E1,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", + // 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+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // 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 */ "\u00E8,\u00E9,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // 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+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, + /* ~ morekeys_t */ + // U+00B7: "·" MIDDLE DOT + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "l\u00B7l,\u0142", + /* morekeys_g ~ */ + 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_symbols_1 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, 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 + /* keyspec_spanish_row2_10 */ "\u00E7", + }; + + /* Locale cs: Czech */ + private static final String[] TEXTS_cs = { + // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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", + // 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 + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // 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+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", + /* keylabel_to_alpha */ null, + // 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", + // 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+0165: "ť" LATIN SMALL LETTER T WITH CARON + /* morekeys_t */ "\u0165", + /* morekeys_l */ null, + /* morekeys_g */ null, + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + /* keyspec_currency */ null, + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + /* morekeys_r */ "\u0159", + }; + + /* Locale da: Danish */ + private static final String[] TEXTS_da = { + // 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", + // 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", + // 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", + // 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", + /* keylabel_to_alpha */ null, + // 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", + // 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 + /* morekeys_d */ "\u00F0", + /* morekeys_z */ null, + /* morekeys_t */ null, + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u0142", + /* morekeys_g */ null, + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + /* keyspec_currency ~ */ + null, null, null, null, + /* ~ morekeys_cyrillic_ie */ + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + /* keyspec_nordic_row1_11 */ "\u00E5", + // U+00E6: "æ" LATIN SMALL LETTER AE + /* keyspec_nordic_row2_10 */ "\u00E6", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* keyspec_nordic_row2_11 */ "\u00F8", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* morekeys_nordic_row2_10 */ "\u00E4", + /* keyspec_east_slavic_row1_9 ~ */ + null, null, null, null, null, + /* ~ morekeys_cyrillic_soft_sign */ + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* morekeys_nordic_row2_11 */ "\u00F6", + }; + + /* Locale de: German */ + private static final String[] TEXTS_de = { + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E4,%,\u00E2,\u00E0,\u00E1,\u00E6,\u00E3,\u00E5,\u0101", + // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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+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", + // 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", + /* keylabel_to_alpha */ null, + // 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", + /* morekeys_y ~ */ + null, null, null, null, null, null, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + /* keyspec_currency ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, 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 */ + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + /* keyspec_swiss_row1_11 */ "\u00FC", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* keyspec_swiss_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* keyspec_swiss_row2_11 */ "\u00E4", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + /* morekeys_swiss_row1_11 */ "\u00E8", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + /* morekeys_swiss_row2_10 */ "\u00E9", + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + /* morekeys_swiss_row2_11 */ "\u00E0", + }; + + /* Locale el: Greek */ + private static final String[] TEXTS_el = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // Label for "switch to alphabetic" key. + // U+0391: "Α" GREEK CAPITAL LETTER ALPHA + // U+0392: "Β" GREEK CAPITAL LETTER BETA + // U+0393: "Γ" GREEK CAPITAL LETTER GAMMA + /* keylabel_to_alpha */ "\u0391\u0392\u0393", + }; + + /* Locale en: English */ + private static final String[] TEXTS_en = { + // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // 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+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // 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 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", + // 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+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, + /* keylabel_to_alpha */ null, + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + /* morekeys_s */ "\u00DF", + }; + + /* Locale eo: Esperanto */ + private static final String[] TEXTS_eo = { + // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u0103,\u0105,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // 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+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 + // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + // U+00B5: "µ" MICRO SIGN + /* morekeys_u */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B,\u0169,\u0171,\u0173,\u00B5", + // 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 + // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // 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 + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + // 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, + /* keylabel_to_alpha */ 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", + // 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+0165: "ť" LATIN SMALL LETTER T WITH CARON + // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW + // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE + /* morekeys_t */ "\u0165,\u021B,\u0163,\u0167", + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u013A,\u013C,\u013E,\u0140,\u0142", + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + /* morekeys_g */ "\u011F,\u0121,\u0123", + /* single_angle_quotes ~ */ + null, null, null, + /* ~ keyspec_currency */ + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + /* morekeys_r */ "\u0159,\u0155,\u0157", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + // U+0138: "ĸ" LATIN SMALL LETTER KRA + /* morekeys_k */ "\u0137,\u0138", + /* morekeys_cyrillic_ie ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, 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 + /* morekeys_h */ "\u0125,\u0127", + // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX + /* morekeys_w */ "w,\u0175", + /* morekeys_east_slavic_row2_2 ~ */ + null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_tablet_punctuation */ + // 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, null, + /* ~ morekeys_symbols_percent */ + // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX + /* morekeys_v */ "w,\u0175", + /* morekeys_j */ null, + /* morekeys_q */ "q", + /* morekeys_x */ "x", + // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX + /* keyspec_q */ "\u015D", + // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX + /* keyspec_w */ "\u011D", + // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE + /* keyspec_y */ "\u016D", + // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX + /* keyspec_x */ "\u0109", + }; + + /* Locale es: Spanish */ + private static final String[] TEXTS_es = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // 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,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // 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+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, 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 + /* morekeys_punctuation */ "!autoColumnOrder!9,\\,,?,!,#,),(,/,;,\u00A1,',@,:,-,\",+,\\%,&,\u00BF", + }; + + /* Locale et_EE: Estonian (Estonia) */ + private static final String[] TEXTS_et_EE = { + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // 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+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + /* morekeys_a */ "\u00E4,\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E5,\u00E6,\u0105", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + // 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+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + /* morekeys_u */ "\u00FC,\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u016F,\u0171", + // 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 + // 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+0131: "ı" LATIN SMALL LETTER DOTLESS I + /* morekeys_i */ "\u012B,\u00EC,\u012F,\u00ED,\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+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", + /* keylabel_to_alpha */ null, + // 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", + // 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+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + /* morekeys_t */ "\u0163,\u0165", + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + /* morekeys_l */ "\u013C,\u0142,\u013A,\u013E", + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u0123,\u011F", + /* single_angle_quotes ~ */ + null, null, null, + /* ~ keyspec_currency */ + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + /* morekeys_r */ "\u0157,\u0159,\u0155", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + /* morekeys_k */ "\u0137", + /* morekeys_cyrillic_ie */ null, + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + /* keyspec_nordic_row1_11 */ "\u00FC", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* keyspec_nordic_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* keyspec_nordic_row2_11 */ "\u00E4", + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + /* morekeys_nordic_row2_10 */ "\u00F5", + }; + + /* Locale eu_ES: Basque (Spain) */ + private static final String[] TEXTS_eu_ES = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // 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,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // 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+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, null, null, + /* ~ single_quotes */ + // Label for "switch to alphabetic" key. + // U+0627: "ا" ARABIC LETTER ALEF + // U+200C: ZERO WIDTH NON-JOINER + // U+0628: "ب" ARABIC LETTER BEH + // U+067E: "پ" ARABIC LETTER PEH + /* keylabel_to_alpha */ "\u0627\u200C\u0628\u200C\u067E", + /* morekeys_s ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ double_angle_quotes */ + // U+FDFC: "﷼" RIAL SIGN + /* keyspec_currency */ "\uFDFC", + /* morekeys_r ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_punctuation */ + // U+06F1: "۱" EXTENDED ARABIC-INDIC DIGIT ONE + /* keyspec_symbols_1 */ "\u06F1", + // U+06F2: "۲" EXTENDED ARABIC-INDIC DIGIT TWO + /* keyspec_symbols_2 */ "\u06F2", + // U+06F3: "۳" EXTENDED ARABIC-INDIC DIGIT THREE + /* keyspec_symbols_3 */ "\u06F3", + // U+06F4: "۴" EXTENDED ARABIC-INDIC DIGIT FOUR + /* keyspec_symbols_4 */ "\u06F4", + // U+06F5: "۵" EXTENDED ARABIC-INDIC DIGIT FIVE + /* keyspec_symbols_5 */ "\u06F5", + // U+06F6: "۶" EXTENDED ARABIC-INDIC DIGIT SIX + /* keyspec_symbols_6 */ "\u06F6", + // U+06F7: "۷" EXTENDED ARABIC-INDIC DIGIT SEVEN + /* keyspec_symbols_7 */ "\u06F7", + // U+06F8: "۸" EXTENDED ARABIC-INDIC DIGIT EIGHT + /* keyspec_symbols_8 */ "\u06F8", + // U+06F9: "۹" EXTENDED ARABIC-INDIC DIGIT NINE + /* keyspec_symbols_9 */ "\u06F9", + // U+06F0: "۰" EXTENDED ARABIC-INDIC DIGIT ZERO + /* keyspec_symbols_0 */ "\u06F0", + // Label for "switch to symbols" key. + // U+061F: "؟" ARABIC QUESTION MARK + /* keylabel_to_symbol */ "\u06F3\u06F2\u06F1\u061F", + /* additional_morekeys_symbols_1 */ "1", + /* additional_morekeys_symbols_2 */ "2", + /* additional_morekeys_symbols_3 */ "3", + /* additional_morekeys_symbols_4 */ "4", + /* additional_morekeys_symbols_5 */ "5", + /* additional_morekeys_symbols_6 */ "6", + /* additional_morekeys_symbols_7 */ "7", + /* additional_morekeys_symbols_8 */ "8", + /* additional_morekeys_symbols_9 */ "9", + // U+066B: "٫" ARABIC DECIMAL SEPARATOR + // U+066C: "٬" ARABIC THOUSANDS SEPARATOR + /* additional_morekeys_symbols_0 */ "0,\u066B,\u066C", + // U+060C: "،" ARABIC COMMA + // U+061B: "؛" ARABIC SEMICOLON + // U+061F: "؟" ARABIC QUESTION MARK + // 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, + /* ~ morekeys_swiss_row2_11 */ + // U+2605: "★" BLACK STAR + // U+066D: "٭" ARABIC FIVE POINTED STAR + /* morekeys_star */ "\u2605,\u066D", + /* keyspec_left_parenthesis */ "(|)", + /* keyspec_right_parenthesis */ ")|(", + /* keyspec_left_square_bracket */ "[|]", + /* keyspec_right_square_bracket */ "]|[", + /* keyspec_left_curly_bracket */ "{|}", + /* keyspec_right_curly_bracket */ "}|{", + /* keyspec_less_than */ "<|>", + /* keyspec_greater_than */ ">|<", + /* keyspec_less_than_equal */ "\u2264|\u2265", + /* keyspec_greater_than_equal */ "\u2265|\u2264", + /* keyspec_left_double_angle_quote */ "\u00AB|\u00BB", + /* keyspec_right_double_angle_quote */ "\u00BB|\u00AB", + /* keyspec_left_single_angle_quote */ "\u2039|\u203A", + /* keyspec_right_single_angle_quote */ "\u203A|\u2039", + /* 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 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ keyspec_spanish_row2_10 */ + // U+266A: "♪" EIGHTH NOTE + /* morekeys_bullet */ "\u266A", + // The all letters need to be mirrored are found at + // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt + // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS + // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS + /* morekeys_left_parenthesis */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,!text/keyspecs_left_parenthesis_more_keys", + /* morekeys_right_parenthesis */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,!text/keyspecs_right_parenthesis_more_keys", + // U+0655: "ٕ" ARABIC HAMZA BELOW + // U+0652: "ْ" ARABIC SUKUN + // U+0651: "ّ" ARABIC SHADDA + // U+064C: "ٌ" ARABIC DAMMATAN + // U+064D: "ٍ" ARABIC KASRATAN + // U+064B: "ً" ARABIC FATHATAN + // U+0654: "ٔ" ARABIC HAMZA ABOVE + // U+0656: "ٖ" ARABIC SUBSCRIPT ALEF + // U+0670: "ٰ" ARABIC LETTER SUPERSCRIPT ALEF + // U+0653: "ٓ" ARABIC MADDAH ABOVE + // U+064F: "ُ" ARABIC DAMMA + // U+0650: "ِ" ARABIC KASRA + // U+064E: "َ" ARABIC FATHA + // U+0640: "ـ" ARABIC TATWEEL + // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. + // 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", + // U+060C: "،" ARABIC COMMA + /* keyspec_comma */ "\u060C", + /* 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", + // U+066A: "٪" ARABIC PERCENT SIGN + /* keyspec_symbols_percent */ "\u066A", + /* 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, + /* ~ morekeys_plus */ + // U+2264: "≤" LESS-THAN OR EQUAL TO + // U+2265: "≥" GREATER-THAN EQUAL TO + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK + // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + /* morekeys_less_than */ "!fixedColumnOrder!3,!text/keyspec_left_single_angle_quote,!text/keyspec_less_than_equal,!text/keyspec_less_than", + /* morekeys_greater_than */ "!fixedColumnOrder!3,!text/keyspec_right_single_angle_quote,!text/keyspec_greater_than_equal,!text/keyspec_greater_than", + }; + + /* Locale fi: Finnish */ + private static final String[] TEXTS_fi = { + // 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", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // 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+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", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + /* morekeys_u */ "\u00FC", + /* morekeys_e ~ */ + null, null, null, null, null, null, null, + /* ~ keylabel_to_alpha */ + // 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", + /* morekeys_y */ null, + /* morekeys_d */ null, + // 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, null, + /* ~ morekeys_cyrillic_ie */ + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + /* keyspec_nordic_row1_11 */ "\u00E5", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* keyspec_nordic_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* keyspec_nordic_row2_11 */ "\u00E4", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* morekeys_nordic_row2_10 */ "\u00F8", + /* keyspec_east_slavic_row1_9 ~ */ + null, null, null, null, null, + /* ~ morekeys_cyrillic_soft_sign */ + // U+00E6: "æ" LATIN SMALL LETTER AE + /* morekeys_nordic_row2_11 */ "\u00E6", + }; + + /* Locale fr: French */ + private static final String[] TEXTS_fr = { + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E0,\u00E2,%,\u00E6,\u00E1,\u00E4,\u00E3,\u00E5,\u0101,\u00AA", + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // 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 + // 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", + // 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 + // 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+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 */ "\u00EE,%,\u00EF,\u00EC,\u00ED,\u012F,\u012B", + // 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, + /* ~ morekeys_s */ + // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS + /* morekeys_y */ "%,\u00FF", + /* morekeys_d ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, 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 */ + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + /* keyspec_swiss_row1_11 */ "\u00E8", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + /* keyspec_swiss_row2_10 */ "\u00E9", + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + /* keyspec_swiss_row2_11 */ "\u00E0", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + /* morekeys_swiss_row1_11 */ "\u00FC", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* morekeys_swiss_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* morekeys_swiss_row2_11 */ "\u00E4", + }; + + /* Locale gl_ES: Gallegan (Spain) */ + private static final String[] TEXTS_gl_ES = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // 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,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // 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+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, null, null, + /* ~ single_quotes */ + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ double_angle_quotes */ + // U+20B9: "₹" INDIAN RUPEE SIGN + /* keyspec_currency */ "\u20B9", + /* morekeys_r ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_punctuation */ + // U+0967: "१" DEVANAGARI DIGIT ONE + /* keyspec_symbols_1 */ "\u0967", + // U+0968: "२" DEVANAGARI DIGIT TWO + /* keyspec_symbols_2 */ "\u0968", + // U+0969: "३" DEVANAGARI DIGIT THREE + /* keyspec_symbols_3 */ "\u0969", + // U+096A: "४" DEVANAGARI DIGIT FOUR + /* keyspec_symbols_4 */ "\u096A", + // U+096B: "५" DEVANAGARI DIGIT FIVE + /* keyspec_symbols_5 */ "\u096B", + // U+096C: "६" DEVANAGARI DIGIT SIX + /* keyspec_symbols_6 */ "\u096C", + // U+096D: "७" DEVANAGARI DIGIT SEVEN + /* keyspec_symbols_7 */ "\u096D", + // U+096E: "८" DEVANAGARI DIGIT EIGHT + /* keyspec_symbols_8 */ "\u096E", + // U+096F: "९" DEVANAGARI DIGIT NINE + /* keyspec_symbols_9 */ "\u096F", + // U+0966: "०" DEVANAGARI DIGIT ZERO + /* keyspec_symbols_0 */ "\u0966", + // Label for "switch to symbols" key. + /* keylabel_to_symbol */ "?\u0967\u0968\u0969", + /* additional_morekeys_symbols_1 */ "1", + /* additional_morekeys_symbols_2 */ "2", + /* additional_morekeys_symbols_3 */ "3", + /* additional_morekeys_symbols_4 */ "4", + /* additional_morekeys_symbols_5 */ "5", + /* additional_morekeys_symbols_6 */ "6", + /* additional_morekeys_symbols_7 */ "7", + /* additional_morekeys_symbols_8 */ "8", + /* additional_morekeys_symbols_9 */ "9", + /* additional_morekeys_symbols_0 */ "0", + }; + + /* Locale hr: Croatian */ + private static final String[] TEXTS_hr = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // 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", + /* keylabel_to_alpha */ null, + // 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", + /* 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", + /* morekeys_t ~ */ + null, null, null, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + }; + + /* Locale hu: Hungarian */ + private static final String[] TEXTS_hu = { + // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE 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+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", + // 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 + // 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+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // 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+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00ED,\u00EE,\u00EF,\u00EC,\u012F,\u012B", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_rqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_rqm", + /* keylabel_to_alpha ~ */ + null, null, null, null, null, null, null, null, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + }; + + /* Locale hy_AM: Armenian (Armenia) */ + private static final String[] TEXTS_hy_AM = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // 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_s ~ */ + 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+055E: "՞" ARMENIAN QUESTION MARK + // U+055C: "՜" ARMENIAN EXCLAMATION MARK + // U+055A: "՚" ARMENIAN APOSTROPHE + // U+0559: "ՙ" ARMENIAN MODIFIER LETTER LEFT HALF RING + // U+055D: "՝" ARMENIAN COMMA + // U+055B: "՛" ARMENIAN EMPHASIS MARK + // U+058A: "֊" ARMENIAN HYPHEN + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+055F: "՟" ARMENIAN ABBREVIATION MARK + /* morekeys_punctuation */ "!autoColumnOrder!8,\\,,\u055E,\u055C,.,\u055A,\u0559,?,!,\u055D,\u055B,\u058A,\u00BB,\u00AB,\u055F,;,:", + /* keyspec_symbols_1 ~ */ + 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 */ + // U+058F: "֏" ARMENIAN DRAM SIGN + // TODO: Enable this when we have glyph for the following letter + // <string name="keyspec_currency">֏</string> + // + // U+055D: "՝" ARMENIAN COMMA + /* keyspec_tablet_comma */ "\u055D", + /* 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, null, + /* ~ keyhintlabel_period */ + /* 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, 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, + /* ~ morekeys_greater_than */ + // U+055C: "՜" ARMENIAN EXCLAMATION MARK + // U+00A1: "¡" INVERTED EXCLAMATION MARK + /* morekeys_exclamation */ "\u055C,\u00A1", + }; + + /* Locale is: Icelandic */ + private static final String[] TEXTS_is = { + // 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+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,\u00E6,\u00E5,\u00E0,\u00E2,\u00E3,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // 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,\u00EB,\u00E8,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // 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_c */ null, + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + /* keylabel_to_alpha */ null, + /* morekeys_s */ 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 + /* morekeys_d */ "\u00F0", + /* morekeys_z */ null, + // U+00FE: "þ" LATIN SMALL LETTER THORN + /* morekeys_t */ "\u00FE", + }; + + /* Locale it: Italian */ + private static final String[] TEXTS_it = { + // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u00AA", + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // 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", + // 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+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+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // 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+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 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, 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 */ + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + /* keyspec_swiss_row1_11 */ "\u00FC", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* keyspec_swiss_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* keyspec_swiss_row2_11 */ "\u00E4", + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + /* morekeys_swiss_row1_11 */ "\u00E8", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + /* morekeys_swiss_row2_10 */ "\u00E9", + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + /* morekeys_swiss_row2_11 */ "\u00E0", + }; + + /* Locale iw: Hebrew */ + private static final String[] TEXTS_iw = { + /* morekeys_a ~ */ + null, null, null, null, null, null, + /* ~ morekeys_c */ + /* double_quotes */ "!text/double_rqm_9qm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_rqm_9qm", + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ double_angle_quotes */ + // U+20AA: "₪" NEW SHEQEL SIGN + /* keyspec_currency */ "\u20AA", + /* 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, null, null, null, null, null, null, + /* ~ morekeys_swiss_row2_11 */ + // U+2605: "★" BLACK STAR + /* morekeys_star */ "\u2605", + // The all letters need to be mirrored are found at + // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt + // U+2264: "≤" LESS-THAN OR EQUAL TO + // U+2265: "≥" GREATER-THAN EQUAL TO + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK + // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + /* keyspec_left_parenthesis */ "(|)", + /* keyspec_right_parenthesis */ ")|(", + /* keyspec_left_square_bracket */ "[|]", + /* keyspec_right_square_bracket */ "]|[", + /* keyspec_left_curly_bracket */ "{|}", + /* keyspec_right_curly_bracket */ "}|{", + /* keyspec_less_than */ "<|>", + /* keyspec_greater_than */ ">|<", + /* keyspec_less_than_equal */ "\u2264|\u2265", + /* keyspec_greater_than_equal */ "\u2265|\u2264", + /* keyspec_left_double_angle_quote */ "\u00AB|\u00BB", + /* keyspec_right_double_angle_quote */ "\u00BB|\u00AB", + /* keyspec_left_single_angle_quote */ "\u2039|\u203A", + /* keyspec_right_single_angle_quote */ "\u203A|\u2039", + /* morekeys_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, + /* ~ morekeys_currency_dollar */ + // U+00B1: "±" PLUS-MINUS SIGN + // U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN + /* morekeys_plus */ "\u00B1,\uFB29", + }; + + /* Locale ka_GE: Georgian (Georgia) */ + private static final String[] TEXTS_ka_GE = { + /* morekeys_a ~ */ + null, null, null, null, null, null, + /* ~ morekeys_c */ + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + // 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", + }; + + /* Locale kk: Kazakh */ + private static final String[] TEXTS_kk = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_k */ + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* morekeys_cyrillic_ie */ "\u0451", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, + /* ~ morekeys_nordic_row2_10 */ + // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA + /* keyspec_east_slavic_row1_9 */ "\u0449", + // U+044B: "ы" CYRILLIC SMALL LETTER YERU + /* keyspec_east_slavic_row2_2 */ "\u044B", + // U+044D: "э" CYRILLIC SMALL LETTER E + /* keyspec_east_slavic_row2_11 */ "\u044D", + // U+0438: "и" CYRILLIC SMALL LETTER I + /* keyspec_east_slavic_row3_5 */ "\u0438", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* morekeys_cyrillic_soft_sign */ "\u044A", + /* morekeys_nordic_row2_11 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, 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", + // U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U + // U+04B1: "ұ" CYRILLIC SMALL LETTER STRAIGHT U WITH STROKE + /* morekeys_cyrillic_u */ "\u04AF,\u04B1", + // U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER + /* morekeys_cyrillic_en */ "\u04A3", + // U+0493: "ғ" CYRILLIC SMALL LETTER GHE WITH STROKE + /* morekeys_cyrillic_ghe */ "\u0493", + // U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O + /* 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, + /* ~ keyspec_x */ + // U+04BB: "һ" CYRILLIC SMALL LETTER SHHA + /* morekeys_east_slavic_row2_11 */ "\u04BB", + // U+049B: "қ" CYRILLIC SMALL LETTER KA WITH DESCENDER + /* morekeys_cyrillic_ka */ "\u049B", + // U+04D9: "ә" CYRILLIC SMALL LETTER SCHWA + /* morekeys_cyrillic_a */ "\u04D9", + }; + + /* Locale km_KH: Khmer (Cambodia) */ + private static final String[] TEXTS_km_KH = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, 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", + }; + + /* Locale ky: Kirghiz */ + private static final String[] TEXTS_ky = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_k */ + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* morekeys_cyrillic_ie */ "\u0451", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, + /* ~ morekeys_nordic_row2_10 */ + // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA + /* keyspec_east_slavic_row1_9 */ "\u0449", + // U+044B: "ы" CYRILLIC SMALL LETTER YERU + /* keyspec_east_slavic_row2_2 */ "\u044B", + // U+044D: "э" CYRILLIC SMALL LETTER E + /* keyspec_east_slavic_row2_11 */ "\u044D", + // U+0438: "и" CYRILLIC SMALL LETTER I + /* keyspec_east_slavic_row3_5 */ "\u0438", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* morekeys_cyrillic_soft_sign */ "\u044A", + /* morekeys_nordic_row2_11 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, 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", + // U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER + /* morekeys_cyrillic_en */ "\u04A3", + /* morekeys_cyrillic_ghe */ null, + // U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O + /* morekeys_cyrillic_o */ "\u04E9", + }; + + /* Locale lo_LA: Lao (Laos) */ + private static final String[] TEXTS_lo_LA = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ double_angle_quotes */ + // U+20AD: "₭" KIP SIGN + /* keyspec_currency */ "\u20AD", + }; + + /* Locale lt: Lithuanian */ + private static final String[] TEXTS_lt = { + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // 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+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + /* morekeys_a */ "\u0105,\u00E4,\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E5,\u00E6", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // 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+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + /* morekeys_u */ "\u016B,\u0173,\u00FC,\u016B,\u00F9,\u00FA,\u00FB,\u016F,\u0171", + // 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 + // 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+0131: "ı" LATIN SMALL LETTER DOTLESS I + /* morekeys_i */ "\u012F,\u012B,\u00EC,\u00ED,\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+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", + /* keylabel_to_alpha */ null, + // 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", + // 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+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + /* morekeys_t */ "\u0163,\u0165", + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + /* morekeys_l */ "\u013C,\u0142,\u013A,\u013E", + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u0123,\u011F", + /* single_angle_quotes ~ */ + null, null, null, + /* ~ keyspec_currency */ + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + /* morekeys_r */ "\u0157,\u0159,\u0155", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + /* morekeys_k */ "\u0137", + }; + + /* Locale lv: Latvian */ + private static final String[] TEXTS_lv = { + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + /* morekeys_a */ "\u0101,\u00E0,\u00E1,\u00E2,\u00E3,\u00E4,\u00E5,\u00E6,\u0105", + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + // 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+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + /* morekeys_u */ "\u016B,\u0173,\u00F9,\u00FA,\u00FB,\u00FC,\u016F,\u0171", + // 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 + // 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+0131: "ı" LATIN SMALL LETTER DOTLESS I + /* morekeys_i */ "\u012B,\u012F,\u00EC,\u00ED,\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+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", + /* keylabel_to_alpha */ null, + // 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", + // 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+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + /* morekeys_t */ "\u0163,\u0165", + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + /* morekeys_l */ "\u013C,\u0142,\u013A,\u013E", + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u0123,\u011F", + /* single_angle_quotes ~ */ + null, null, null, + /* ~ keyspec_currency */ + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + /* morekeys_r */ "\u0157,\u0159,\u0155", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + /* morekeys_k */ "\u0137", + }; + + /* Locale mk: Macedonian */ + private static final String[] TEXTS_mk = { + /* morekeys_a ~ */ + null, null, null, null, null, null, + /* ~ morekeys_c */ + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + // 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_s ~ */ + null, 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", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, 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", + // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE + /* keyspec_south_slavic_row1_6 */ "\u0455", + // U+045C: "ќ" CYRILLIC SMALL LETTER KJE + /* keyspec_south_slavic_row2_11 */ "\u045C", + // U+0437: "з" CYRILLIC SMALL LETTER ZE + /* keyspec_south_slavic_row3_1 */ "\u0437", + // U+0453: "ѓ" CYRILLIC SMALL LETTER GJE + /* keyspec_south_slavic_row3_8 */ "\u0453", + }; + + /* Locale mn_MN: Mongolian (Mongolia) */ + private static final String[] TEXTS_mn_MN = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ double_angle_quotes */ + // U+20AE: "₮" TUGRIK SIGN + /* keyspec_currency */ "\u20AE", + }; + + /* Locale my_MM: Burmese (Myanmar) */ + private static final String[] TEXTS_my_MM = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // 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_s ~ */ + 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 */ + /* morekeys_punctuation */ "!autoColumnOrder!9,\u104A,.,?,!,#,),(,/,;,...,',@,:,-,\",+,\\%,&", + /* keyspec_symbols_1 ~ */ + 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 */ + // U+104A: "၊" MYANMAR SIGN LITTLE SECTION + // U+104B: "။" MYANMAR SIGN SECTION + /* keyspec_tablet_comma */ "\u104A", + /* 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, + /* ~ keyspec_right_single_angle_quote */ + /* morekeys_tablet_comma */ "\\,", + /* keyhintlabel_period */ "\u104A", + /* morekeys_tablet_period ~ */ + null, 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, 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // 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", + // 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", + // 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 + // 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, + /* double_quotes */ "!text/double_9qm_rqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_rqm", + /* keylabel_to_alpha ~ */ + null, null, 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", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* keyspec_nordic_row2_10 */ "\u00F8", + // U+00E6: "æ" LATIN SMALL LETTER AE + /* keyspec_nordic_row2_11 */ "\u00E6", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* morekeys_nordic_row2_10 */ "\u00F6", + /* keyspec_east_slavic_row1_9 ~ */ + null, null, null, null, null, + /* ~ morekeys_cyrillic_soft_sign */ + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* morekeys_nordic_row2_11 */ "\u00E4", + }; + + /* Locale ne_NP: Nepali (Nepal) */ + private static final String[] TEXTS_ne_NP = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ double_angle_quotes */ + // U+0930/U+0941/U+002E "रु." NEPALESE RUPEE SIGN + /* keyspec_currency */ "\u0930\u0941.", + /* morekeys_r ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_punctuation */ + // U+0967: "१" DEVANAGARI DIGIT ONE + /* keyspec_symbols_1 */ "\u0967", + // U+0968: "२" DEVANAGARI DIGIT TWO + /* keyspec_symbols_2 */ "\u0968", + // U+0969: "३" DEVANAGARI DIGIT THREE + /* keyspec_symbols_3 */ "\u0969", + // U+096A: "४" DEVANAGARI DIGIT FOUR + /* keyspec_symbols_4 */ "\u096A", + // U+096B: "५" DEVANAGARI DIGIT FIVE + /* keyspec_symbols_5 */ "\u096B", + // U+096C: "६" DEVANAGARI DIGIT SIX + /* keyspec_symbols_6 */ "\u096C", + // U+096D: "७" DEVANAGARI DIGIT SEVEN + /* keyspec_symbols_7 */ "\u096D", + // U+096E: "८" DEVANAGARI DIGIT EIGHT + /* keyspec_symbols_8 */ "\u096E", + // U+096F: "९" DEVANAGARI DIGIT NINE + /* keyspec_symbols_9 */ "\u096F", + // U+0966: "०" DEVANAGARI DIGIT ZERO + /* keyspec_symbols_0 */ "\u0966", + // Label for "switch to symbols" key. + /* keylabel_to_symbol */ "?\u0967\u0968\u0969", + /* additional_morekeys_symbols_1 */ "1", + /* additional_morekeys_symbols_2 */ "2", + /* additional_morekeys_symbols_3 */ "3", + /* additional_morekeys_symbols_4 */ "4", + /* additional_morekeys_symbols_5 */ "5", + /* additional_morekeys_symbols_6 */ "6", + /* additional_morekeys_symbols_7 */ "7", + /* additional_morekeys_symbols_8 */ "8", + /* additional_morekeys_symbols_9 */ "9", + /* additional_morekeys_symbols_0 */ "0", + }; + + /* Locale nl: Dutch */ + private static final String[] TEXTS_nl = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E1,\u00E4,\u00E2,\u00E0,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // 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,\u00EB,\u00EA,\u00E8,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // 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", + /* keylabel_to_alpha */ null, + /* morekeys_s */ null, + // U+0133: "ij" LATIN SMALL LIGATURE IJ + /* morekeys_y */ "\u0133", + }; + + /* Locale pl: Polish */ + private static final String[] TEXTS_pl = { + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u0105,\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // 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+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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, + // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK + // 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+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, + // 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", + /* keylabel_to_alpha */ null, + // 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", + /* morekeys_y */ null, + /* morekeys_d */ 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_t */ null, + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u0142", + }; + + /* Locale pt: Portuguese */ + private static final String[] TEXTS_pt = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E3,\u00E0,\u00E2,\u00E4,\u00E5,\u00E6,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // 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", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // 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 + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + /* morekeys_e */ "\u00E9,\u00EA,\u00E8,\u0119,\u0117,\u0113,\u00EB", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + /* morekeys_i */ "\u00ED,\u00EE,\u00EC,\u00EF,\u012F,\u012B", + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + /* morekeys_c */ "\u00E7,\u010D,\u0107", + }; + + /* Locale rm: Raeto-Romance */ + private static final String[] TEXTS_rm = { + /* morekeys_a */ null, + // 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+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + /* morekeys_o */ "\u00F2,\u00F3,\u00F6,\u00F4,\u00F5,\u0153,\u00F8", + }; + + /* Locale ro: Romanian */ + private static final String[] TEXTS_ro = { + // 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_o ~ */ + null, null, null, + /* ~ morekeys_e */ + // 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 */ "\u00EE,\u00EF,\u00EC,\u00ED,\u012F,\u012B", + /* morekeys_c */ null, + /* double_quotes */ "!text/double_9qm_rqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_rqm", + /* keylabel_to_alpha */ null, + // 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", + /* morekeys_y ~ */ + null, null, null, + /* ~ morekeys_z */ + // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW + /* morekeys_t */ "\u021B", + }; + + /* Locale ru: Russian */ + private static final String[] TEXTS_ru = { + /* morekeys_a ~ */ + null, null, null, null, null, null, + /* ~ morekeys_c */ + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~ morekeys_k */ + // U+0451: "ё" CYRILLIC SMALL LETTER IO + /* morekeys_cyrillic_ie */ "\u0451", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, + /* ~ morekeys_nordic_row2_10 */ + // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA + /* keyspec_east_slavic_row1_9 */ "\u0449", + // U+044B: "ы" CYRILLIC SMALL LETTER YERU + /* keyspec_east_slavic_row2_2 */ "\u044B", + // U+044D: "э" CYRILLIC SMALL LETTER E + /* keyspec_east_slavic_row2_11 */ "\u044D", + // U+0438: "и" CYRILLIC SMALL LETTER I + /* keyspec_east_slavic_row3_5 */ "\u0438", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* morekeys_cyrillic_soft_sign */ "\u044A", + }; + + /* Locale sk: Slovak */ + private static final String[] TEXTS_sk = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // 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+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + /* morekeys_a */ "\u00E1,\u00E4,\u0101,\u00E0,\u00E2,\u00E3,\u00E5,\u00E6,\u0105", + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + /* morekeys_u */ "\u00FA,\u016F,\u00FC,\u016B,\u0173,\u00F9,\u00FB,\u0171", + // 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 + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // 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", + /* keylabel_to_alpha */ null, + // 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", + // 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+0165: "ť" LATIN SMALL LETTER T WITH CARON + // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + /* morekeys_t */ "\u0165,\u0163", + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u013E,\u013A,\u013C,\u0142", + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u0123,\u011F", + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + /* keyspec_currency */ null, + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + /* morekeys_r */ "\u0155,\u0159,\u0157", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + /* morekeys_k */ "\u0137", + }; + + /* Locale sl: Slovenian */ + private static final String[] TEXTS_sl = { + /* morekeys_a ~ */ + null, null, null, null, null, + /* ~ morekeys_i */ + // 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", + /* keylabel_to_alpha */ null, + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + /* morekeys_s */ "\u0161", + /* 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", + /* morekeys_t ~ */ + null, null, null, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + }; + + /* Locale sr: Serbian */ + private static final String[] TEXTS_sr = { + /* morekeys_a ~ */ + null, null, null, null, null, null, + /* ~ morekeys_c */ + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + // END: More keys definitions for Serbian (Cyrillic) + // 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_s ~ */ + null, null, null, null, null, null, null, + /* ~ morekeys_g */ + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + /* keyspec_currency ~ */ + null, null, null, + /* ~ morekeys_k */ + // U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE + /* morekeys_cyrillic_ie */ "\u0450", + /* keyspec_nordic_row1_11 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, 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", + // TODO: Move these to sr-Latn once we can handle IETF language tag with script name specified. + // BEGIN: More keys definitions for Serbian (Latin) + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // <string name="morekeys_s">š,ß,ś</string> + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // <string name="morekeys_c">č,ç,ć</string> + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + // <string name="morekeys_d">ď</string> + // 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 + // <string name="morekeys_z">ž,ź,ż</string> + // END: More keys definitions for Serbian (Latin) + // BEGIN: More keys definitions for Serbian (Cyrillic) + // U+0437: "з" CYRILLIC SMALL LETTER ZE + /* keyspec_south_slavic_row1_6 */ "\u0437", + // U+045B: "ћ" CYRILLIC SMALL LETTER TSHE + /* keyspec_south_slavic_row2_11 */ "\u045B", + // U+0455: "ѕ" CYRILLIC SMALL LETTER DZE + /* keyspec_south_slavic_row3_1 */ "\u0455", + // U+0452: "ђ" CYRILLIC SMALL LETTER DJE + /* keyspec_south_slavic_row3_8 */ "\u0452", + }; + + /* Locale sv: Swedish */ + private static final String[] TEXTS_sv = { + // 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", + // 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", + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // 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 */ "\u00FC,\u00FA,\u00F9,\u00FB,\u016B", + // 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+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, + /* keylabel_to_alpha */ 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", + // 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+0165: "ť" LATIN SMALL LETTER T WITH CARON + // U+00FE: "þ" LATIN SMALL LETTER THORN + /* morekeys_t */ "\u0165,\u00FE", + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u0142", + /* morekeys_g */ null, + /* single_angle_quotes */ "!text/single_raqm_laqm", + /* double_angle_quotes */ "!text/double_raqm_laqm", + /* keyspec_currency */ null, + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + /* morekeys_r */ "\u0159", + /* morekeys_k */ null, + /* morekeys_cyrillic_ie */ null, + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + /* keyspec_nordic_row1_11 */ "\u00E5", + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + /* keyspec_nordic_row2_10 */ "\u00F6", + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + /* keyspec_nordic_row2_11 */ "\u00E4", + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + /* morekeys_nordic_row2_10 */ "\u00F8,\u0153", + /* keyspec_east_slavic_row1_9 ~ */ + null, null, null, null, null, + /* ~ morekeys_cyrillic_soft_sign */ + // U+00E6: "æ" LATIN SMALL LETTER AE + /* morekeys_nordic_row2_11 */ "\u00E6", + }; + + /* Locale sw: Swahili */ + private static final String[] TEXTS_sw = { + // This is the same as English except morekeys_g. + // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // 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+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // 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 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", + // 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+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, + /* keylabel_to_alpha */ null, + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + /* morekeys_s */ "\u00DF", + /* morekeys_y ~ */ + null, null, null, null, null, + /* ~ morekeys_l */ + /* morekeys_g */ "g\'", + }; + + /* Locale th: Thai */ + private static final String[] TEXTS_th = { + /* morekeys_a ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ single_quotes */ + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ double_angle_quotes */ + // U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT + /* keyspec_currency */ "\u0E3F", + }; + + /* Locale tl: Tagalog */ + private static final String[] TEXTS_tl = { + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA", + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // 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", + // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE + // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE + // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS + // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX + // 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,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113", + // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE + // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX + // 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+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+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+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", + /* 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 + // 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+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, + /* ~ keylabel_to_alpha */ + // 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", + /* morekeys_y ~ */ + null, null, null, null, null, + /* ~ morekeys_l */ + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + /* morekeys_g */ "\u011F", + }; + + /* Locale uk: Ukrainian */ + private static final String[] TEXTS_uk = { + /* morekeys_a ~ */ + null, null, null, null, null, null, + /* ~ morekeys_c */ + /* double_quotes */ "!text/double_9qm_lqm", + /* morekeys_n */ null, + /* single_quotes */ "!text/single_9qm_lqm", + // 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_s ~ */ + null, null, null, null, null, null, null, null, null, + /* ~ double_angle_quotes */ + // U+20B4: "₴" HRYVNIA SIGN + /* keyspec_currency */ "\u20B4", + /* morekeys_r ~ */ + null, null, null, null, null, null, null, + /* ~ morekeys_nordic_row2_10 */ + // U+0449: "щ" CYRILLIC SMALL LETTER SHCHA + /* keyspec_east_slavic_row1_9 */ "\u0449", + // U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I + /* keyspec_east_slavic_row2_2 */ "\u0456", + // U+0454: "є" CYRILLIC SMALL LETTER UKRAINIAN IE + /* keyspec_east_slavic_row2_11 */ "\u0454", + // U+0438: "и" CYRILLIC SMALL LETTER I + /* keyspec_east_slavic_row3_5 */ "\u0438", + // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN + /* morekeys_cyrillic_soft_sign */ "\u044A", + /* morekeys_nordic_row2_11 ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, 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", + /* morekeys_cyrillic_u */ null, + /* morekeys_cyrillic_en */ null, + // U+0491: "ґ" CYRILLIC SMALL LETTER GHE WITH UPTURN + /* morekeys_cyrillic_ghe */ "\u0491", + }; + + /* Locale vi: Vietnamese */ + private static final String[] TEXTS_vi = { + // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE + // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE + // U+1EA3: "ả" LATIN SMALL LETTER A WITH HOOK ABOVE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+1EA1: "ạ" LATIN SMALL LETTER A WITH DOT BELOW + // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE + // U+1EB1: "ằ" LATIN SMALL LETTER A WITH BREVE AND GRAVE + // U+1EAF: "ắ" LATIN SMALL LETTER A WITH BREVE AND ACUTE + // U+1EB3: "ẳ" LATIN SMALL LETTER A WITH BREVE AND HOOK ABOVE + // U+1EB5: "ẵ" LATIN SMALL LETTER A WITH BREVE AND TILDE + // U+1EB7: "ặ" LATIN SMALL LETTER A WITH BREVE AND DOT BELOW + // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX + // U+1EA7: "ầ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND GRAVE + // U+1EA5: "ấ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND ACUTE + // U+1EA9: "ẩ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND HOOK ABOVE + // U+1EAB: "ẫ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND TILDE + // U+1EAD: "ậ" LATIN SMALL LETTER A WITH CIRCUMFLEX AND DOT BELOW + /* morekeys_a */ "\u00E0,\u00E1,\u1EA3,\u00E3,\u1EA1,\u0103,\u1EB1,\u1EAF,\u1EB3,\u1EB5,\u1EB7,\u00E2,\u1EA7,\u1EA5,\u1EA9,\u1EAB,\u1EAD", + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+1ECF: "ỏ" LATIN SMALL LETTER O WITH HOOK ABOVE + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+1ECD: "ọ" LATIN SMALL LETTER O WITH DOT BELOW + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+1ED3: "ồ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND GRAVE + // U+1ED1: "ố" LATIN SMALL LETTER O WITH CIRCUMFLEX AND ACUTE + // U+1ED5: "ổ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND HOOK ABOVE + // U+1ED7: "ỗ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND TILDE + // U+1ED9: "ộ" LATIN SMALL LETTER O WITH CIRCUMFLEX AND DOT BELOW + // U+01A1: "ơ" LATIN SMALL LETTER O WITH HORN + // U+1EDD: "ờ" LATIN SMALL LETTER O WITH HORN AND GRAVE + // U+1EDB: "ớ" LATIN SMALL LETTER O WITH HORN AND ACUTE + // U+1EDF: "ở" LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE + // 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+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE + // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE + // U+1EE7: "ủ" LATIN SMALL LETTER U WITH HOOK ABOVE + // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE + // U+1EE5: "ụ" LATIN SMALL LETTER U WITH DOT BELOW + // U+01B0: "ư" LATIN SMALL LETTER U WITH HORN + // U+1EEB: "ừ" LATIN SMALL LETTER U WITH HORN AND GRAVE + // U+1EE9: "ứ" LATIN SMALL LETTER U WITH HORN AND ACUTE + // U+1EED: "ử" LATIN SMALL LETTER U WITH HORN AND HOOK ABOVE + // U+1EEF: "ữ" LATIN SMALL LETTER U WITH HORN AND TILDE + // U+1EF1: "ự" LATIN SMALL LETTER U WITH HORN AND DOT BELOW + /* morekeys_u */ "\u00F9,\u00FA,\u1EE7,\u0169,\u1EE5,\u01B0,\u1EEB,\u1EE9,\u1EED,\u1EEF,\u1EF1", + // 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 ~ */ + null, null, null, null, null, null, + /* ~ morekeys_s */ + // U+1EF3: "ỳ" LATIN SMALL LETTER Y WITH GRAVE + // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE + // U+1EF7: "ỷ" LATIN SMALL LETTER Y WITH HOOK ABOVE + // U+1EF9: "ỹ" LATIN SMALL LETTER Y WITH TILDE + // U+1EF5: "ỵ" LATIN SMALL LETTER Y WITH DOT BELOW + /* morekeys_y */ "\u1EF3,\u00FD,\u1EF7,\u1EF9,\u1EF5", + // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE + /* morekeys_d */ "\u0111", + /* morekeys_z ~ */ + null, null, null, null, null, null, + /* ~ double_angle_quotes */ + // U+20AB: "₫" DONG SIGN + /* keyspec_currency */ "\u20AB", + }; + + /* Locale zu: Zulu */ + private static final String[] TEXTS_zu = { + // This is the same as English + // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + /* morekeys_a */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101", + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+0153: "œ" LATIN SMALL LIGATURE OE + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // 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+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX + // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS + // 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 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B", + // 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+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, + /* keylabel_to_alpha */ null, + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + /* morekeys_s */ "\u00DF", + }; + + /* Locale zz: Alphabet */ + private static final String[] TEXTS_zz = { + // 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+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS + // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE + // U+00E6: "æ" LATIN SMALL LETTER AE + // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON + // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE + // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK + // U+00AA: "ª" FEMININE ORDINAL INDICATOR + /* morekeys_a */ "\u00E0,\u00E1,\u00E2,\u00E3,\u00E4,\u00E5,\u00E6,\u0101,\u0103,\u0105,\u00AA", + // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE + // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE + // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX + // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE + // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS + // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE + // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON + // U+014F: "ŏ" LATIN SMALL LETTER O WITH BREVE + // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE + // 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+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+0169: "ũ" LATIN SMALL LETTER U WITH TILDE + // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON + // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE + // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE + // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE + // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK + /* morekeys_u */ "\u00F9,\u00FA,\u00FB,\u00FC,\u0169,\u016B,\u016D,\u016F,\u0171,\u0173", + // 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 + // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS + // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE + // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON + // U+012D: "ĭ" LATIN SMALL LETTER I WITH BREVE + // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK + // 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 + // U+0148: "ň" LATIN SMALL LETTER N WITH CARON + // 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, + /* keylabel_to_alpha */ null, + // U+00DF: "ß" LATIN SMALL LETTER SHARP S + // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE + // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX + // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA + // U+0161: "š" LATIN SMALL LETTER S WITH CARON + // U+017F: "ſ" LATIN SMALL LETTER LONG S + /* morekeys_s */ "\u00DF,\u015B,\u015D,\u015F,\u0161,\u017F", + // 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+00FE: "þ" LATIN SMALL LETTER THORN + // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA + // U+0165: "ť" LATIN SMALL LETTER T WITH CARON + // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE + /* morekeys_t */ "\u00FE,\u0163,\u0165,\u0167", + // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE + // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA + // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON + // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT + // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE + /* morekeys_l */ "\u013A,\u013C,\u013E,\u0140,\u0142", + // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX + // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE + // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE + // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA + /* morekeys_g */ "\u011D,\u011F,\u0121,\u0123", + /* single_angle_quotes ~ */ + null, null, null, + /* ~ keyspec_currency */ + // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE + // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA + // U+0159: "ř" LATIN SMALL LETTER R WITH CARON + /* morekeys_r */ "\u0155,\u0157,\u0159", + // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA + // U+0138: "ĸ" LATIN SMALL LETTER KRA + /* morekeys_k */ "\u0137,\u0138", + /* morekeys_cyrillic_ie ~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, 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", + // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX + /* 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, + /* ~ morekeys_v */ + // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX + /* morekeys_j */ "\u0135", + }; + + private static final Object[] LOCALES_AND_TEXTS = { + // "locale", TEXT_ARRAY, /* numberOfNonNullText/lengthOf_TEXT_ARRAY localeName */ + "DEFAULT", TEXTS_DEFAULT, /* 168/168 DEFAULT */ + "af" , TEXTS_af, /* 7/ 12 Afrikaans */ + "ar" , TEXTS_ar, /* 55/110 Arabic */ + "az_AZ" , TEXTS_az_AZ, /* 8/ 17 Azerbaijani (Azerbaijan) */ + "be_BY" , TEXTS_be_BY, /* 9/ 32 Belarusian (Belarus) */ + "bg" , TEXTS_bg, /* 2/ 10 Bulgarian */ + "ca" , TEXTS_ca, /* 11/ 95 Catalan */ + "cs" , TEXTS_cs, /* 17/ 21 Czech */ + "da" , TEXTS_da, /* 19/ 33 Danish */ + "de" , TEXTS_de, /* 16/ 62 German */ + "el" , TEXTS_el, /* 1/ 10 Greek */ + "en" , TEXTS_en, /* 8/ 11 English */ + "eo" , TEXTS_eo, /* 26/118 Esperanto */ + "es" , TEXTS_es, /* 8/ 34 Spanish */ + "et_EE" , TEXTS_et_EE, /* 22/ 27 Estonian (Estonia) */ + "eu_ES" , TEXTS_eu_ES, /* 7/ 8 Basque (Spain) */ + "fa" , TEXTS_fa, /* 58/125 Persian */ + "fi" , TEXTS_fi, /* 10/ 33 Finnish */ + "fr" , TEXTS_fr, /* 13/ 62 French */ + "gl_ES" , TEXTS_gl_ES, /* 7/ 8 Gallegan (Spain) */ + "hi" , TEXTS_hi, /* 23/ 55 Hindi */ + "hr" , TEXTS_hr, /* 9/ 19 Croatian */ + "hu" , TEXTS_hu, /* 9/ 19 Hungarian */ + "hy_AM" , TEXTS_hy_AM, /* 8/126 Armenian (Armenia) */ + "is" , TEXTS_is, /* 10/ 15 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) */ + "ky" , TEXTS_ky, /* 10/ 88 Kirghiz */ + "lo_LA" , TEXTS_lo_LA, /* 2/ 20 Lao (Laos) */ + "lt" , TEXTS_lt, /* 18/ 22 Lithuanian */ + "lv" , TEXTS_lv, /* 18/ 22 Latvian */ + "mk" , TEXTS_mk, /* 9/ 93 Macedonian */ + "mn_MN" , TEXTS_mn_MN, /* 2/ 20 Mongolian (Mongolia) */ + "my_MM" , TEXTS_my_MM, /* 8/104 Burmese (Myanmar) */ + "nb" , TEXTS_nb, /* 11/ 33 Norwegian Bokmål */ + "ne_NP" , TEXTS_ne_NP, /* 23/ 55 Nepali (Nepal) */ + "nl" , TEXTS_nl, /* 9/ 12 Dutch */ + "pl" , TEXTS_pl, /* 10/ 16 Polish */ + "pt" , TEXTS_pt, /* 6/ 6 Portuguese */ + "rm" , TEXTS_rm, /* 1/ 2 Raeto-Romance */ + "ro" , TEXTS_ro, /* 6/ 15 Romanian */ + "ru" , TEXTS_ru, /* 9/ 32 Russian */ + "sk" , TEXTS_sk, /* 20/ 22 Slovak */ + "sl" , TEXTS_sl, /* 8/ 19 Slovenian */ + "sr" , TEXTS_sr, /* 11/ 93 Serbian */ + "sv" , TEXTS_sv, /* 21/ 33 Swedish */ + "sw" , TEXTS_sw, /* 9/ 17 Swahili */ + "th" , TEXTS_th, /* 2/ 20 Thai */ + "tl" , TEXTS_tl, /* 7/ 8 Tagalog */ + "tr" , TEXTS_tr, /* 7/ 17 Turkish */ + "uk" , TEXTS_uk, /* 11/ 87 Ukrainian */ + "vi" , TEXTS_vi, /* 8/ 20 Vietnamese */ + "zu" , TEXTS_zu, /* 8/ 11 Zulu */ + "zz" , TEXTS_zz, /* 19/112 Alphabet */ + }; + + static { + for (int index = 0; index < NAMES.length; index++) { + sNameToIndexesMap.put(NAMES[index], index); + } + + for (int i = 0; i < LOCALES_AND_TEXTS.length; i += 2) { + final String locale = (String)LOCALES_AND_TEXTS[i]; + final String[] textsTable = (String[])LOCALES_AND_TEXTS[i + 1]; + sLocaleToTextsTableMap.put(locale, textsTable); + sTextsTableToLocaleMap.put(textsTable, locale); + } + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java new file mode 100644 index 000000000..6400a2440 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java @@ -0,0 +1,68 @@ +/* + * 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.keyboard.internal; + +import android.view.inputmethod.InputMethodSubtype; + +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; + +import java.util.Collections; +import java.util.List; + +/** + * This class determines that the language name on the spacebar should be displayed in what format. + */ +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; + + private List<InputMethodSubtype> mEnabledSubtypes = Collections.emptyList(); + private boolean mIsSystemLanguageSameAsInputLanguage; + + public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) { + if (SubtypeLocaleUtils.isNoLanguage(subtype)) { + 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 String keyboardLayout = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype); + int sameLanguageAndLayoutCount = 0; + for (final InputMethodSubtype ims : mEnabledSubtypes) { + final String language = SubtypeLocaleUtils.getSubtypeLocale(ims).getLanguage(); + if (keyboardLanguage.equals(language) && keyboardLayout.equals( + SubtypeLocaleUtils.getKeyboardLayoutSetName(ims))) { + sameLanguageAndLayoutCount++; + } + } + // Display full locale name only when there are multiple subtypes that have the same + // locale and keyboard layout. Otherwise displaying language name is enough. + return sameLanguageAndLayoutCount > 1 ? FORMAT_TYPE_FULL_LOCALE + : FORMAT_TYPE_LANGUAGE_ONLY; + } + + public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { + mEnabledSubtypes = enabledSubtypes; + } + + public void updateIsSystemLanguageSameAsInputLanguage(final boolean isSame) { + mIsSystemLanguageSameAsInputLanguage = isSame; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java index 110936f8f..56ef4767f 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java +++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java @@ -18,23 +18,42 @@ package com.android.inputmethod.keyboard.internal; import android.text.TextUtils; +import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.StringUtils; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Locale; +/** + * The more key specification object. The more keys are an array of {@link MoreKeySpec}. + * + * The more keys specification is comma separated "key specification" each of which represents one + * "more key". + * The key specification might have label or string resource reference in it. These references are + * expanded before parsing comma. + * Special character, comma ',' backslash '\' can be escaped by '\' character. + * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)} + * as well. + */ +// TODO: Should extend the key specification object. public final class MoreKeySpec { public final int mCode; public final String mLabel; public final String mOutputText; public final int mIconId; - public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale, - final KeyboardCodesSet codesSet) { - mLabel = KeySpecParser.toUpperCaseOfStringForLocale( + public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale) { + if (TextUtils.isEmpty(moreKeySpec)) { + throw new KeySpecParser.KeySpecParserError("Empty more key spec"); + } + mLabel = StringUtils.toUpperCaseOfStringForLocale( KeySpecParser.getLabel(moreKeySpec), needsToUpperCase, locale); - final int code = KeySpecParser.toUpperCaseOfCodeForLocale( - KeySpecParser.getCode(moreKeySpec, codesSet), needsToUpperCase, locale); + final int code = StringUtils.toUpperCaseOfCodeForLocale( + KeySpecParser.getCode(moreKeySpec), needsToUpperCase, locale); if (code == Constants.CODE_UNSPECIFIED) { // Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters // upper case representation ("SS"). @@ -42,12 +61,19 @@ public final class MoreKeySpec { mOutputText = mLabel; } else { mCode = code; - mOutputText = KeySpecParser.toUpperCaseOfStringForLocale( + mOutputText = StringUtils.toUpperCaseOfStringForLocale( KeySpecParser.getOutputText(moreKeySpec), needsToUpperCase, locale); } mIconId = KeySpecParser.getIconId(moreKeySpec); } + public Key buildKey(final int x, final int y, final int labelFlags, + final KeyboardParams params) { + return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags, + Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultKeyWidth, params.mDefaultRowHeight, + params.mHorizontalGap, params.mVerticalGap); + } + @Override public int hashCode() { int hashCode = 1; @@ -74,7 +100,7 @@ public final class MoreKeySpec { @Override public String toString() { final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel - : KeySpecParser.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId)); + : KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId)); final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText : Constants.printableCode(mCode)); if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) { @@ -83,4 +109,196 @@ public final class MoreKeySpec { return label + "|" + output; } } + + private static final boolean DEBUG = LatinImeLogger.sDBG; + // Constants for parsing. + private static final char COMMA = Constants.CODE_COMMA; + private static final char BACKSLASH = Constants.CODE_BACKSLASH; + private static final String ADDITIONAL_MORE_KEY_MARKER = + StringUtils.newSingleCodePointString(Constants.CODE_PERCENT); + + /** + * Split the text containing multiple key specifications separated by commas into an array of + * key specifications. + * A key specification can contain a character escaped by the backslash character, including a + * comma character. + * Note that an empty key specification will be eliminated from the result array. + * + * @param text the text containing multiple key specifications. + * @return an array of key specification text. Null if the specified <code>text</code> is empty + * or has no key specifications. + */ + public static String[] splitKeySpecs(final String text) { + if (TextUtils.isEmpty(text)) { + return null; + } + final int size = text.length(); + // Optimization for one-letter key specification. + if (size == 1) { + return text.charAt(0) == COMMA ? null : new String[] { text }; + } + + ArrayList<String> list = null; + int start = 0; + // The characters in question in this loop are COMMA and BACKSLASH. These characters never + // match any high or low surrogate character. So it is OK to iterate through with char + // index. + for (int pos = 0; pos < size; pos++) { + final char c = text.charAt(pos); + if (c == COMMA) { + // Skip empty entry. + if (pos - start > 0) { + if (list == null) { + list = CollectionUtils.newArrayList(); + } + list.add(text.substring(start, pos)); + } + // Skip comma + start = pos + 1; + } else if (c == BACKSLASH) { + // Skip escape character and escaped character. + pos++; + } + } + final String remain = (size - start > 0) ? text.substring(start) : null; + if (list == null) { + return remain != null ? new String[] { remain } : null; + } + if (remain != null) { + list.add(remain); + } + return list.toArray(new String[list.size()]); + } + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + private static String[] filterOutEmptyString(final String[] array) { + if (array == null) { + return EMPTY_STRING_ARRAY; + } + ArrayList<String> out = null; + for (int i = 0; i < array.length; i++) { + final String entry = array[i]; + if (TextUtils.isEmpty(entry)) { + if (out == null) { + out = CollectionUtils.arrayAsList(array, 0, i); + } + } else if (out != null) { + out.add(entry); + } + } + if (out == null) { + return array; + } + return out.toArray(new String[out.size()]); + } + + public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs, + final String[] additionalMoreKeySpecs) { + final String[] moreKeys = filterOutEmptyString(moreKeySpecs); + final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs); + final int moreKeysCount = moreKeys.length; + final int additionalCount = additionalMoreKeys.length; + ArrayList<String> out = null; + int additionalIndex = 0; + for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) { + final String moreKeySpec = moreKeys[moreKeyIndex]; + if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) { + if (additionalIndex < additionalCount) { + // Replace '%' marker with additional more key specification. + final String additionalMoreKey = additionalMoreKeys[additionalIndex]; + if (out != null) { + out.add(additionalMoreKey); + } else { + moreKeys[moreKeyIndex] = additionalMoreKey; + } + additionalIndex++; + } else { + // Filter out excessive '%' marker. + if (out == null) { + out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex); + } + } + } else { + if (out != null) { + out.add(moreKeySpec); + } + } + } + if (additionalCount > 0 && additionalIndex == 0) { + // No '%' marker is found in more keys. + // Insert all additional more keys to the head of more keys. + if (DEBUG && out != null) { + throw new RuntimeException("Internal logic error:" + + " moreKeys=" + Arrays.toString(moreKeys) + + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); + } + out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount); + for (int i = 0; i < moreKeysCount; i++) { + out.add(moreKeys[i]); + } + } else if (additionalIndex < additionalCount) { + // The number of '%' markers are less than additional more keys. + // Append remained additional more keys to the tail of more keys. + if (DEBUG && out != null) { + throw new RuntimeException("Internal logic error:" + + " moreKeys=" + Arrays.toString(moreKeys) + + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); + } + out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount); + for (int i = additionalIndex; i < additionalCount; i++) { + out.add(additionalMoreKeys[additionalIndex]); + } + } + if (out == null && moreKeysCount > 0) { + return moreKeys; + } else if (out != null && out.size() > 0) { + return out.toArray(new String[out.size()]); + } else { + return null; + } + } + + public static int getIntValue(final String[] moreKeys, final String key, + final int defaultValue) { + if (moreKeys == null) { + return defaultValue; + } + final int keyLen = key.length(); + boolean foundValue = false; + int value = defaultValue; + for (int i = 0; i < moreKeys.length; i++) { + final String moreKeySpec = moreKeys[i]; + if (moreKeySpec == null || !moreKeySpec.startsWith(key)) { + continue; + } + moreKeys[i] = null; + try { + if (!foundValue) { + value = Integer.parseInt(moreKeySpec.substring(keyLen)); + foundValue = true; + } + } catch (NumberFormatException e) { + throw new RuntimeException( + "integer should follow after " + key + ": " + moreKeySpec); + } + } + return value; + } + + public static boolean getBooleanValue(final String[] moreKeys, final String key) { + if (moreKeys == null) { + return false; + } + boolean value = false; + for (int i = 0; i < moreKeys.length; i++) { + final String moreKeySpec = moreKeys[i]; + if (moreKeySpec == null || !moreKeySpec.equals(key)) { + continue; + } + moreKeys[i] = null; + value = true; + } + return value; + } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java b/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java index a0935b985..3a9aa81a3 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java +++ b/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java @@ -20,18 +20,19 @@ import android.util.Log; import android.view.MotionEvent; import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.KeyDetector; import com.android.inputmethod.keyboard.PointerTracker; -import com.android.inputmethod.keyboard.PointerTracker.KeyEventHandler; import com.android.inputmethod.latin.utils.CoordinateUtils; public final class NonDistinctMultitouchHelper { private static final String TAG = NonDistinctMultitouchHelper.class.getSimpleName(); + private static final int MAIN_POINTER_TRACKER_ID = 0; private int mOldPointerCount = 1; private Key mOldKey; private int[] mLastCoords = CoordinateUtils.newInstance(); - public void processMotionEvent(final MotionEvent me, final KeyEventHandler keyEventHandler) { + public void processMotionEvent(final MotionEvent me, final KeyDetector keyDetector) { final int pointerCount = me.getPointerCount(); final int oldPointerCount = mOldPointerCount; mOldPointerCount = pointerCount; @@ -41,8 +42,9 @@ public final class NonDistinctMultitouchHelper { return; } - // Use only main (id=0) pointer tracker. - final PointerTracker mainTracker = PointerTracker.getPointerTracker(0, keyEventHandler); + // Use only main pointer tracker. + final PointerTracker mainTracker = PointerTracker.getPointerTracker( + MAIN_POINTER_TRACKER_ID); final int action = me.getActionMasked(); final int index = me.getActionIndex(); final long eventTime = me.getEventTime(); @@ -51,12 +53,12 @@ public final class NonDistinctMultitouchHelper { // In single-touch. if (oldPointerCount == 1 && pointerCount == 1) { if (me.getPointerId(index) == mainTracker.mPointerId) { - mainTracker.processMotionEvent(me, keyEventHandler); + mainTracker.processMotionEvent(me, keyDetector); return; } // Inject a copied event. injectMotionEvent(action, me.getX(index), me.getY(index), downTime, eventTime, - mainTracker, keyEventHandler); + mainTracker, keyDetector); return; } @@ -70,7 +72,7 @@ public final class NonDistinctMultitouchHelper { mOldKey = mainTracker.getKeyOn(x, y); // Inject an artifact up event for the old key. injectMotionEvent(MotionEvent.ACTION_UP, x, y, downTime, eventTime, - mainTracker, keyEventHandler); + mainTracker, keyDetector); return; } @@ -85,11 +87,11 @@ public final class NonDistinctMultitouchHelper { // Inject an artifact down event for the new key. // An artifact up event for the new key will usually be injected as a single-touch. injectMotionEvent(MotionEvent.ACTION_DOWN, x, y, downTime, eventTime, - mainTracker, keyEventHandler); + mainTracker, keyDetector); if (action == MotionEvent.ACTION_UP) { // Inject an artifact up event for the new key also. injectMotionEvent(MotionEvent.ACTION_UP, x, y, downTime, eventTime, - mainTracker, keyEventHandler); + mainTracker, keyDetector); } } return; @@ -101,11 +103,11 @@ public final class NonDistinctMultitouchHelper { private static void injectMotionEvent(final int action, final float x, final float y, final long downTime, final long eventTime, final PointerTracker tracker, - final KeyEventHandler handler) { + final KeyDetector keyDetector) { final MotionEvent me = MotionEvent.obtain( downTime, eventTime, action, x, y, 0 /* metaState */); try { - tracker.processMotionEvent(me, handler); + tracker.processMotionEvent(me, keyDetector); } finally { me.recycle(); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java index 7ee45e8f6..5ac34188c 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java +++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java @@ -28,7 +28,7 @@ public final class PointerTrackerQueue { public interface Element { public boolean isModifier(); - public boolean isInSlidingKeyInput(); + public boolean isInDraggingFinger(); public void onPhantomUpEvent(long eventTime); public void cancelTrackingForAction(); } @@ -193,13 +193,13 @@ public final class PointerTrackerQueue { } } - public boolean isAnyInSlidingKeyInput() { + public boolean isAnyInDraggingFinger() { synchronized (mExpandableArrayOfActivePointers) { final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers; final int arraySize = mArraySize; for (int index = 0; index < arraySize; index++) { final Element element = expandableArray.get(index); - if (element.isInSlidingKeyInput()) { + if (element.isInDraggingFinger()) { return true; } } diff --git a/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java b/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java deleted file mode 100644 index d1ccdc7b5..000000000 --- a/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.keyboard.internal; - -import android.content.Context; -import android.util.AttributeSet; -import android.widget.ScrollView; - -/** - * This is an extended {@link ScrollView} that can notify - * {@link ScrollView#onScrollChanged(int,int,int,int} and - * {@link ScrollView#onOverScrolled(int,int,int,int)} to a content view. - */ -public class ScrollViewWithNotifier extends ScrollView { - private ScrollListener mScrollListener = EMPTY_LISTER; - - public interface ScrollListener { - public void notifyScrollChanged(int scrollX, int scrollY, int oldX, int oldY); - public void notifyOverScrolled(int scrollX, int scrollY, boolean clampedX, - boolean clampedY); - } - - private static final ScrollListener EMPTY_LISTER = new ScrollListener() { - @Override - public void notifyScrollChanged(int scrollX, int scrollY, int oldX, int oldY) {} - @Override - public void notifyOverScrolled(int scrollX, int scrollY, boolean clampedX, - boolean clampedY) {} - }; - - public ScrollViewWithNotifier(final Context context, final AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onScrollChanged(final int scrollX, final int scrollY, final int oldX, - final int oldY) { - super.onScrollChanged(scrollX, scrollY, oldX, oldY); - mScrollListener.notifyScrollChanged(scrollX, scrollY, oldX, oldY); - } - - @Override - protected void onOverScrolled(final int scrollX, final int scrollY, final boolean clampedX, - final boolean clampedY) { - super.onOverScrolled(scrollX, scrollY, clampedX, clampedY); - mScrollListener.notifyOverScrolled(scrollX, scrollY, clampedX, clampedY); - } - - public void setScrollListener(final ScrollListener listener) { - mScrollListener = listener; - } -} diff --git a/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java b/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputDrawingPreview.java index 2787ebfb9..76cb89160 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java +++ b/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputDrawingPreview.java @@ -29,7 +29,7 @@ import com.android.inputmethod.latin.utils.CoordinateUtils; /** * Draw rubber band preview graphics during sliding key input. */ -public final class SlidingKeyInputPreview extends AbstractDrawingPreview { +public final class SlidingKeyInputDrawingPreview extends AbstractDrawingPreview { private final float mPreviewBodyRadius; private boolean mShowsSlidingKeyInputPreview; @@ -40,7 +40,8 @@ public final class SlidingKeyInputPreview extends AbstractDrawingPreview { private final RoundedLine mRoundedLine = new RoundedLine(); private final Paint mPaint = new Paint(); - public SlidingKeyInputPreview(final View drawingView, final TypedArray mainKeyboardViewAttr) { + public SlidingKeyInputDrawingPreview(final View drawingView, + final TypedArray mainKeyboardViewAttr) { super(drawingView); final int previewColor = mainKeyboardViewAttr.getColor( R.styleable.MainKeyboardView_slidingKeyInputPreviewColor, 0); @@ -61,6 +62,11 @@ public final class SlidingKeyInputPreview extends AbstractDrawingPreview { mPaint.setColor(previewColor); } + @Override + public void onDeallocateMemory() { + // Nothing to do here. + } + public void dismissSlidingKeyInputPreview() { mShowsSlidingKeyInputPreview = false; getDrawingView().invalidate(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java b/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java new file mode 100644 index 000000000..ec7b9b024 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +import android.os.Message; +import android.os.SystemClock; +import android.view.ViewConfiguration; + +import com.android.inputmethod.keyboard.Key; +import com.android.inputmethod.keyboard.PointerTracker; +import com.android.inputmethod.keyboard.PointerTracker.TimerProxy; +import com.android.inputmethod.keyboard.internal.TimerHandler.Callbacks; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; + +// TODO: Separate this class into KeyTimerHandler and BatchInputTimerHandler or so. +public final class TimerHandler extends LeakGuardHandlerWrapper<Callbacks> implements TimerProxy { + public interface Callbacks { + public void startWhileTypingFadeinAnimation(); + public void startWhileTypingFadeoutAnimation(); + public void onLongPress(PointerTracker tracker); + } + + private static final int MSG_TYPING_STATE_EXPIRED = 0; + private static final int MSG_REPEAT_KEY = 1; + private static final int MSG_LONGPRESS_KEY = 2; + private static final int MSG_LONGPRESS_SHIFT_KEY = 3; + private static final int MSG_DOUBLE_TAP_SHIFT_KEY = 4; + private static final int MSG_UPDATE_BATCH_INPUT = 5; + + private final int mIgnoreAltCodeKeyTimeout; + private final int mGestureRecognitionUpdateTime; + + public TimerHandler(final Callbacks ownerInstance, final int ignoreAltCodeKeyTimeout, + final int gestureRecognitionUpdateTime) { + super(ownerInstance); + mIgnoreAltCodeKeyTimeout = ignoreAltCodeKeyTimeout; + mGestureRecognitionUpdateTime = gestureRecognitionUpdateTime; + } + + @Override + public void handleMessage(final Message msg) { + final Callbacks callbacks = getOwnerInstance(); + if (callbacks == null) { + return; + } + final PointerTracker tracker = (PointerTracker) msg.obj; + switch (msg.what) { + case MSG_TYPING_STATE_EXPIRED: + callbacks.startWhileTypingFadeinAnimation(); + break; + case MSG_REPEAT_KEY: + tracker.onKeyRepeat(msg.arg1 /* code */, msg.arg2 /* repeatCount */); + break; + case MSG_LONGPRESS_KEY: + case MSG_LONGPRESS_SHIFT_KEY: + cancelLongPressTimers(); + callbacks.onLongPress(tracker); + break; + case MSG_UPDATE_BATCH_INPUT: + tracker.updateBatchInputByTimer(SystemClock.uptimeMillis()); + startUpdateBatchInputTimer(tracker); + break; + } + } + + @Override + public void startKeyRepeatTimerOf(final PointerTracker tracker, final int repeatCount, + final int delay) { + final Key key = tracker.getKey(); + if (key == null || delay == 0) { + return; + } + sendMessageDelayed( + obtainMessage(MSG_REPEAT_KEY, key.getCode(), repeatCount, tracker), delay); + } + + private void cancelKeyRepeatTimerOf(final PointerTracker tracker) { + removeMessages(MSG_REPEAT_KEY, tracker); + } + + public void cancelKeyRepeatTimers() { + removeMessages(MSG_REPEAT_KEY); + } + + // TODO: Suppress layout changes in key repeat mode + public boolean isInKeyRepeat() { + return hasMessages(MSG_REPEAT_KEY); + } + + @Override + public void startLongPressTimerOf(final PointerTracker tracker, final int delay) { + final Key key = tracker.getKey(); + if (key == null) { + return; + } + // Use a separate message id for long pressing shift key, because long press shift key + // timers should be canceled when other key is pressed. + final int messageId = (key.getCode() == Constants.CODE_SHIFT) + ? MSG_LONGPRESS_SHIFT_KEY : MSG_LONGPRESS_KEY; + sendMessageDelayed(obtainMessage(messageId, tracker), delay); + } + + @Override + public void cancelLongPressTimerOf(final PointerTracker tracker) { + removeMessages(MSG_LONGPRESS_KEY, tracker); + removeMessages(MSG_LONGPRESS_SHIFT_KEY, tracker); + } + + @Override + public void cancelLongPressShiftKeyTimers() { + removeMessages(MSG_LONGPRESS_SHIFT_KEY); + } + + public void cancelLongPressTimers() { + removeMessages(MSG_LONGPRESS_KEY); + removeMessages(MSG_LONGPRESS_SHIFT_KEY); + } + + @Override + public void startTypingStateTimer(final Key typedKey) { + if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) { + return; + } + + final boolean isTyping = isTypingState(); + removeMessages(MSG_TYPING_STATE_EXPIRED); + final Callbacks callbacks = getOwnerInstance(); + if (callbacks == null) { + return; + } + + // When user hits the space or the enter key, just cancel the while-typing timer. + final int typedCode = typedKey.getCode(); + if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) { + if (isTyping) { + callbacks.startWhileTypingFadeinAnimation(); + } + return; + } + + sendMessageDelayed( + obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout); + if (isTyping) { + return; + } + callbacks.startWhileTypingFadeoutAnimation(); + } + + @Override + public boolean isTypingState() { + return hasMessages(MSG_TYPING_STATE_EXPIRED); + } + + @Override + public void startDoubleTapShiftKeyTimer() { + sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP_SHIFT_KEY), + ViewConfiguration.getDoubleTapTimeout()); + } + + @Override + public void cancelDoubleTapShiftKeyTimer() { + removeMessages(MSG_DOUBLE_TAP_SHIFT_KEY); + } + + @Override + public boolean isInDoubleTapShiftKeyTimeout() { + return hasMessages(MSG_DOUBLE_TAP_SHIFT_KEY); + } + + @Override + public void cancelKeyTimersOf(final PointerTracker tracker) { + cancelKeyRepeatTimerOf(tracker); + cancelLongPressTimerOf(tracker); + } + + public void cancelAllKeyTimers() { + cancelKeyRepeatTimers(); + cancelLongPressTimers(); + } + + @Override + public void startUpdateBatchInputTimer(final PointerTracker tracker) { + if (mGestureRecognitionUpdateTime <= 0) { + return; + } + removeMessages(MSG_UPDATE_BATCH_INPUT, tracker); + sendMessageDelayed(obtainMessage(MSG_UPDATE_BATCH_INPUT, tracker), + mGestureRecognitionUpdateTime); + } + + @Override + public void cancelUpdateBatchInputTimer(final PointerTracker tracker) { + removeMessages(MSG_UPDATE_BATCH_INPUT, tracker); + } + + @Override + public void cancelAllUpdateBatchInputTimers() { + removeMessages(MSG_UPDATE_BATCH_INPUT); + } + + public void cancelAllMessages() { + cancelAllKeyTimers(); + cancelAllUpdateBatchInputTimers(); + } +} diff --git a/java/src/com/android/inputmethod/keyboard/internal/TypingTimeRecorder.java b/java/src/com/android/inputmethod/keyboard/internal/TypingTimeRecorder.java new file mode 100644 index 000000000..9593f715c --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/internal/TypingTimeRecorder.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.keyboard.internal; + +public final class TypingTimeRecorder { + private final int mStaticTimeThresholdAfterFastTyping; // msec + private final int mSuppressKeyPreviewAfterBatchInputDuration; + private long mLastTypingTime; + private long mLastLetterTypingTime; + private long mLastBatchInputTime; + + public TypingTimeRecorder(final int staticTimeThresholdAfterFastTyping, + final int suppressKeyPreviewAfterBatchInputDuration) { + mStaticTimeThresholdAfterFastTyping = staticTimeThresholdAfterFastTyping; + mSuppressKeyPreviewAfterBatchInputDuration = suppressKeyPreviewAfterBatchInputDuration; + } + + public boolean isInFastTyping(final long eventTime) { + final long elapsedTimeSinceLastLetterTyping = eventTime - mLastLetterTypingTime; + return elapsedTimeSinceLastLetterTyping < mStaticTimeThresholdAfterFastTyping; + } + + private boolean wasLastInputTyping() { + return mLastTypingTime >= mLastBatchInputTime; + } + + public void onCodeInput(final int code, final long eventTime) { + // Record the letter typing time when + // 1. Letter keys are typed successively without any batch input in between. + // 2. A letter key is typed within the threshold time since the last any key typing. + // 3. A non-letter key is typed within the threshold time since the last letter key typing. + if (Character.isLetter(code)) { + if (wasLastInputTyping() + || eventTime - mLastTypingTime < mStaticTimeThresholdAfterFastTyping) { + mLastLetterTypingTime = eventTime; + } + } else { + if (eventTime - mLastLetterTypingTime < mStaticTimeThresholdAfterFastTyping) { + // This non-letter typing should be treated as a part of fast typing. + mLastLetterTypingTime = eventTime; + } + } + mLastTypingTime = eventTime; + } + + public void onEndBatchInput(final long eventTime) { + mLastBatchInputTime = eventTime; + } + + public long getLastLetterTypingTime() { + return mLastLetterTypingTime; + } + + public boolean needsToSuppressKeyPreviewPopup(final long eventTime) { + return !wasLastInputTyping() + && eventTime - mLastBatchInputTime < mSuppressKeyPreviewAfterBatchInputDuration; + } +} diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java deleted file mode 100644 index 463d09344..000000000 --- a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin; - -import android.content.Context; -import android.util.Log; - -import com.android.inputmethod.latin.makedict.DictEncoder; -import com.android.inputmethod.latin.makedict.UnsupportedFormatException; -import com.android.inputmethod.latin.makedict.Ver3DictEncoder; - -import java.io.File; -import java.io.IOException; -import java.util.Map; - -// TODO: Quit extending Dictionary after implementing dynamic binary dictionary. -abstract public class AbstractDictionaryWriter extends Dictionary { - /** Used for Log actions from this class */ - private static final String TAG = AbstractDictionaryWriter.class.getSimpleName(); - - private final Context mContext; - - public AbstractDictionaryWriter(final Context context, final String dictType) { - super(dictType); - mContext = context; - } - - abstract public void clear(); - - /** - * Add a unigram with an optional shortcut to the dictionary. - * @param word The word to add. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this unigram. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored - * if shortcutTarget is null. - * @param isNotAWord true if this is not a word, i.e. shortcut only. - */ - abstract public void addUnigramWord(final String word, final String shortcutTarget, - final int frequency, final int shortcutFreq, final boolean isNotAWord); - - // TODO: Remove lastModifiedTime after making binary dictionary support forgetting curve. - abstract public void addBigramWords(final String word0, final String word1, - final int frequency, final boolean isValid, - final long lastModifiedTime); - - abstract public void removeBigramWords(final String word0, final String word1); - - abstract protected void writeDictionary(final DictEncoder dictEncoder, - final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException; - - public void write(final String fileName, final Map<String, String> attributeMap) { - final String tempFileName = fileName + ".temp"; - final File file = new File(mContext.getFilesDir(), fileName); - final File tempFile = new File(mContext.getFilesDir(), tempFileName); - try { - final DictEncoder dictEncoder = new Ver3DictEncoder(tempFile); - writeDictionary(dictEncoder, attributeMap); - tempFile.renameTo(file); - } catch (IOException e) { - Log.e(TAG, "IO exception while writing file", e); - } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported format", e); - } - } -} diff --git a/java/src/com/android/inputmethod/latin/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java index 875192554..fd6c24dfe 100644 --- a/java/src/com/android/inputmethod/latin/AssetFileAddress.java +++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java @@ -16,6 +16,8 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.latin.utils.FileUtils; + import java.io.File; /** @@ -52,4 +54,12 @@ public final class AssetFileAddress { if (!f.isFile()) return null; return new AssetFileAddress(filename, offset, length); } + + public boolean pointsToPhysicalFile() { + return 0 == mOffset; + } + + public void deleteUnderlyingFile() { + FileUtils.deleteRecursively(new File(mFilename)); + } } diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index fd296988e..88174ba83 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -17,21 +17,30 @@ package com.android.inputmethod.latin; import android.text.TextUtils; +import android.util.Log; import android.util.SparseArray; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.makedict.DictionaryHeader; +import com.android.inputmethod.latin.makedict.FormatSpec; +import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions; +import com.android.inputmethod.latin.makedict.UnsupportedFormatException; +import com.android.inputmethod.latin.makedict.WordProperty; import com.android.inputmethod.latin.settings.NativeSuggestOptions; +import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.FileUtils; import com.android.inputmethod.latin.utils.JniUtils; +import com.android.inputmethod.latin.utils.LanguageModelParam; import com.android.inputmethod.latin.utils.StringUtils; import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.Locale; -import java.util.Map; /** * Implements a static, compacted, binary dictionary of standard words. @@ -57,17 +66,40 @@ public final class BinaryDictionary extends Dictionary { @UsedForTesting public static final String MAX_BIGRAM_COUNT_QUERY = "MAX_BIGRAM_COUNT"; + public static final int NOT_A_VALID_TIMESTAMP = -1; + + // Format to get unigram flags from native side via getWordPropertyNative(). + private static final int FORMAT_WORD_PROPERTY_OUTPUT_FLAG_COUNT = 4; + 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_SHORTCUTS_INDEX = 3; + + // Format to get probability and historical info from native side via getWordPropertyNative(). + public static final int FORMAT_WORD_PROPERTY_OUTPUT_PROBABILITY_INFO_COUNT = 4; + public static final int FORMAT_WORD_PROPERTY_PROBABILITY_INDEX = 0; + public static final int FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX = 1; + public static final int FORMAT_WORD_PROPERTY_LEVEL_INDEX = 2; + public static final int FORMAT_WORD_PROPERTY_COUNT_INDEX = 3; + + public static final String DICT_FILE_NAME_SUFFIX_FOR_MIGRATION = ".migrate"; + private long mNativeDict; private final Locale mLocale; private final long mDictSize; private final String mDictFilePath; + private final boolean mIsUpdatable; + private boolean mHasUpdated; + private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH]; + private final int[] mOutputSuggestionCount = new int[1]; private final int[] mOutputCodePoints = new int[MAX_WORD_LENGTH * MAX_RESULTS]; private final int[] mSpaceIndices = new int[MAX_RESULTS]; private final int[] mOutputScores = new int[MAX_RESULTS]; private final int[] mOutputTypes = new int[MAX_RESULTS]; // Only one result is ever used private final int[] mOutputAutoCommitFirstWordConfidence = new int[1]; + private final float[] mInputOutputLanguageWeight = new float[1]; private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions(); @@ -107,6 +139,8 @@ public final class BinaryDictionary extends Dictionary { mLocale = locale; mDictSize = length; mDictFilePath = filename; + mIsUpdatable = isUpdatable; + mHasUpdated = false; mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance); loadDictionary(filename, offset, length, isUpdatable); } @@ -115,92 +149,144 @@ public final class BinaryDictionary extends Dictionary { JniUtils.loadNativeLibrary(); } - private static native boolean createEmptyDictFileNative(String filePath, long dictVersion, - String[] attributeKeyStringArray, String[] attributeValueStringArray); private static native long openNative(String sourceDir, long dictOffset, long dictSize, boolean isUpdatable); + private static native void getHeaderInfoNative(long dict, int[] outHeaderSize, + int[] outFormatVersion, ArrayList<int[]> outAttributeKeys, + ArrayList<int[]> outAttributeValues); private static native void flushNative(long dict, String filePath); private static native boolean needsToRunGCNative(long dict, boolean mindsBlockByGC); private static native void flushWithGCNative(long dict, String filePath); private static native void closeNative(long dict); + private static native int getFormatVersionNative(long dict); private static native int getProbabilityNative(long dict, int[] word); private static native int getBigramProbabilityNative(long dict, int[] word0, int[] word1); - private static native int getSuggestionsNative(long dict, long proximityInfo, + private static native void getWordPropertyNative(long dict, int[] word, + int[] outCodePoints, boolean[] outFlags, int[] outProbabilityInfo, + ArrayList<int[]> outBigramTargets, ArrayList<int[]> outBigramProbabilityInfo, + ArrayList<int[]> outShortcutTargets, ArrayList<Integer> outShortcutProbabilities); + private static native int getNextWordNative(long dict, int token, int[] outCodePoints); + private static native void getSuggestionsNative(long dict, long proximityInfo, long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times, - int[] pointerIds, int[] inputCodePoints, int inputSize, int commitPoint, - int[] suggestOptions, int[] prevWordCodePointArray, - int[] outputCodePoints, int[] outputScores, int[] outputIndices, int[] outputTypes, - int[] outputAutoCommitFirstWordConfidence); - private static native float calcNormalizedScoreNative(int[] before, int[] after, int score); - private static native int editDistanceNative(int[] before, int[] after); - private static native void addUnigramWordNative(long dict, int[] word, int probability); + int[] pointerIds, int[] inputCodePoints, int inputSize, int[] suggestOptions, + int[] prevWordCodePointArray, int[] outputSuggestionCount, int[] outputCodePoints, + int[] outputScores, int[] outputIndices, int[] outputTypes, + int[] outputAutoCommitFirstWordConfidence, float[] inOutLanguageWeight); + private static native void addUnigramWordNative(long dict, int[] word, int probability, + int[] shortcutTarget, int shortcutProbability, boolean isNotAWord, + boolean isBlacklisted, int timestamp); private static native void addBigramWordsNative(long dict, int[] word0, int[] word1, - int probability); + int probability, int timestamp); private static native void removeBigramWordsNative(long dict, int[] word0, int[] word1); + private static native int addMultipleDictionaryEntriesNative(long dict, + LanguageModelParam[] languageModelParams, int startIndex); private static native int calculateProbabilityNative(long dict, int unigramProbability, int bigramProbability); private static native String getPropertyNative(long dict, String query); - - @UsedForTesting - public static boolean createEmptyDictFile(final String filePath, final long dictVersion, - final Map<String, String> attributeMap) { - final String[] keyArray = new String[attributeMap.size()]; - final String[] valueArray = new String[attributeMap.size()]; - int index = 0; - for (final String key : attributeMap.keySet()) { - keyArray[index] = key; - valueArray[index] = attributeMap.get(key); - index++; - } - return createEmptyDictFileNative(filePath, dictVersion, keyArray, valueArray); - } + private static native boolean isCorruptedNative(long dict); // TODO: Move native dict into session private final void loadDictionary(final String path, final long startOffset, final long length, final boolean isUpdatable) { + mHasUpdated = false; mNativeDict = openNative(path, startOffset, length, isUpdatable); } + // TODO: Check isCorrupted() for main dictionaries. + public boolean isCorrupted() { + if (!isValidDictionary()) { + return false; + } + if (!isCorruptedNative(mNativeDict)) { + return false; + } + // TODO: Record the corruption. + Log.e(TAG, "BinaryDictionary (" + mDictFilePath + ") is corrupted."); + Log.e(TAG, "locale: " + mLocale); + Log.e(TAG, "dict size: " + mDictSize); + Log.e(TAG, "updatable: " + mIsUpdatable); + return true; + } + + public DictionaryHeader getHeader() throws UnsupportedFormatException { + if (mNativeDict == 0) { + return null; + } + final int[] outHeaderSize = new int[1]; + final int[] outFormatVersion = new int[1]; + final ArrayList<int[]> outAttributeKeys = CollectionUtils.newArrayList(); + final ArrayList<int[]> outAttributeValues = CollectionUtils.newArrayList(); + getHeaderInfoNative(mNativeDict, outHeaderSize, outFormatVersion, outAttributeKeys, + outAttributeValues); + final HashMap<String, String> attributes = new HashMap<String, String>(); + for (int i = 0; i < outAttributeKeys.size(); i++) { + final String attributeKey = StringUtils.getStringFromNullTerminatedCodePointArray( + outAttributeKeys.get(i)); + final String attributeValue = StringUtils.getStringFromNullTerminatedCodePointArray( + outAttributeValues.get(i)); + attributes.put(attributeKey, attributeValue); + } + final boolean hasHistoricalInfo = DictionaryHeader.ATTRIBUTE_VALUE_TRUE.equals( + attributes.get(DictionaryHeader.HAS_HISTORICAL_INFO_KEY)); + return new DictionaryHeader(outHeaderSize[0], new DictionaryOptions(attributes), + new FormatSpec.FormatOptions(outFormatVersion[0], hasHistoricalInfo)); + } + + @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final float[] inOutLanguageWeight) { return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, 0 /* sessionId */); + additionalFeaturesOptions, 0 /* sessionId */, inOutLanguageWeight); } @Override public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, - final int sessionId) { - if (!isValidDictionary()) return null; + final int sessionId, final float[] inOutLanguageWeight) { + if (!isValidDictionary()) { + return null; + } Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE); // TODO: toLowerCase in the native code final int[] prevWordCodePointArray = (null == prevWord) ? null : StringUtils.toCodePointArray(prevWord); - final int composerSize = composer.size(); - + final InputPointers inputPointers = composer.getInputPointers(); final boolean isGesture = composer.isBatchMode(); - if (composerSize <= 1 || !isGesture) { - if (composerSize > MAX_WORD_LENGTH - 1) return null; - for (int i = 0; i < composerSize; i++) { - mInputCodePoints[i] = composer.getCodeAt(i); + final int inputSize; + if (!isGesture) { + inputSize = composer.copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount( + mInputCodePoints); + if (inputSize < 0) { + return null; } + } else { + inputSize = inputPointers.getPointerSize(); } - final InputPointers ips = composer.getInputPointers(); - final int inputSize = isGesture ? ips.getPointerSize() : composerSize; mNativeSuggestOptions.setIsGesture(isGesture); mNativeSuggestOptions.setAdditionalFeaturesOptions(additionalFeaturesOptions); + if (inOutLanguageWeight != null) { + mInputOutputLanguageWeight[0] = inOutLanguageWeight[0]; + } else { + mInputOutputLanguageWeight[0] = Dictionary.NOT_A_LANGUAGE_WEIGHT; + } // proximityInfo and/or prevWordForBigrams may not be null. - final int count = getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(), - getTraverseSession(sessionId).getSession(), ips.getXCoordinates(), - ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), mInputCodePoints, - inputSize, 0 /* commitPoint */, mNativeSuggestOptions.getOptions(), - prevWordCodePointArray, mOutputCodePoints, mOutputScores, mSpaceIndices, - mOutputTypes, mOutputAutoCommitFirstWordConfidence); + getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(), + getTraverseSession(sessionId).getSession(), inputPointers.getXCoordinates(), + inputPointers.getYCoordinates(), inputPointers.getTimes(), + inputPointers.getPointerIds(), mInputCodePoints, inputSize, + mNativeSuggestOptions.getOptions(), prevWordCodePointArray, mOutputSuggestionCount, + mOutputCodePoints, mOutputScores, mSpaceIndices, mOutputTypes, + mOutputAutoCommitFirstWordConfidence, mInputOutputLanguageWeight); + if (inOutLanguageWeight != null) { + inOutLanguageWeight[0] = mInputOutputLanguageWeight[0]; + } + final int count = mOutputSuggestionCount[0]; final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); for (int j = 0; j < count; ++j) { final int start = j * MAX_WORD_LENGTH; @@ -235,18 +321,8 @@ public final class BinaryDictionary extends Dictionary { return mNativeDict != 0; } - public static float calcNormalizedScore(final String before, final String after, - final int score) { - return calcNormalizedScoreNative(StringUtils.toCodePointArray(before), - 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)); + public int getFormatVersion() { + return getFormatVersionNative(mNativeDict); } @Override @@ -274,23 +350,77 @@ public final class BinaryDictionary extends Dictionary { return getBigramProbabilityNative(mNativeDict, codePoints0, codePoints1); } - // Add a unigram entry to binary dictionary in native code. - public void addUnigramWord(final String word, final int probability) { + public WordProperty getWordProperty(final String word) { + if (TextUtils.isEmpty(word)) { + return null; + } + final int[] codePoints = StringUtils.toCodePointArray(word); + final int[] outCodePoints = new int[MAX_WORD_LENGTH]; + 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 = CollectionUtils.newArrayList(); + final ArrayList<int[]> outBigramProbabilityInfo = CollectionUtils.newArrayList(); + final ArrayList<int[]> outShortcutTargets = CollectionUtils.newArrayList(); + final ArrayList<Integer> outShortcutProbabilities = CollectionUtils.newArrayList(); + getWordPropertyNative(mNativeDict, codePoints, outCodePoints, outFlags, outProbabilityInfo, + outBigramTargets, outBigramProbabilityInfo, 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_SHORTCUTS_INDEX], outProbabilityInfo, + outBigramTargets, outBigramProbabilityInfo, outShortcutTargets, + outShortcutProbabilities); + } + + public static class GetNextWordPropertyResult { + public WordProperty mWordProperty; + public int mNextToken; + + public GetNextWordPropertyResult(final WordProperty wordPreperty, final int nextToken) { + mWordProperty = wordPreperty; + mNextToken = nextToken; + } + } + + /** + * Method to iterate all words in the dictionary for makedict. + * If token is 0, this method newly starts iterating the dictionary. + */ + public GetNextWordPropertyResult getNextWordProperty(final int token) { + final int[] codePoints = new int[MAX_WORD_LENGTH]; + final int nextToken = getNextWordNative(mNativeDict, token, codePoints); + final String word = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints); + return new GetNextWordPropertyResult(getWordProperty(word), nextToken); + } + + // Add a unigram entry to binary dictionary with unigram attributes in native code. + public void addUnigramWord(final String word, final int probability, + final String shortcutTarget, final int shortcutProbability, final boolean isNotAWord, + final boolean isBlacklisted, final int timestamp) { if (TextUtils.isEmpty(word)) { return; } final int[] codePoints = StringUtils.toCodePointArray(word); - addUnigramWordNative(mNativeDict, codePoints, probability); + final int[] shortcutTargetCodePoints = (shortcutTarget != null) ? + StringUtils.toCodePointArray(shortcutTarget) : null; + addUnigramWordNative(mNativeDict, codePoints, probability, shortcutTargetCodePoints, + shortcutProbability, isNotAWord, isBlacklisted, timestamp); + mHasUpdated = true; } - // Add a bigram entry to binary dictionary in native code. - public void addBigramWords(final String word0, final String word1, final int probability) { + // Add a bigram entry to binary dictionary with timestamp in native code. + public void addBigramWords(final String word0, final String word1, final int probability, + final int timestamp) { if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) { return; } final int[] codePoints0 = StringUtils.toCodePointArray(word0); final int[] codePoints1 = StringUtils.toCodePointArray(word1); - addBigramWordsNative(mNativeDict, codePoints0, codePoints1, probability); + addBigramWordsNative(mNativeDict, codePoints0, codePoints1, probability, timestamp); + mHasUpdated = true; } // Remove a bigram entry form binary dictionary in native code. @@ -301,19 +431,41 @@ public final class BinaryDictionary extends Dictionary { final int[] codePoints0 = StringUtils.toCodePointArray(word0); final int[] codePoints1 = StringUtils.toCodePointArray(word1); removeBigramWordsNative(mNativeDict, codePoints0, codePoints1); + mHasUpdated = true; + } + + public void addMultipleDictionaryEntries(final LanguageModelParam[] languageModelParams) { + if (!isValidDictionary()) return; + int processedParamCount = 0; + while (processedParamCount < languageModelParams.length) { + if (needsToRunGC(true /* mindsBlockByGC */)) { + flushWithGC(); + } + processedParamCount = addMultipleDictionaryEntriesNative(mNativeDict, + languageModelParams, processedParamCount); + mHasUpdated = true; + if (processedParamCount <= 0) { + return; + } + } } private void reopen() { close(); final File dictFile = new File(mDictFilePath); - mNativeDict = openNative(dictFile.getAbsolutePath(), 0 /* startOffset */, - dictFile.length(), true /* isUpdatable */); + // WARNING: Because we pass 0 as the offset and file.length() as the length, this can + // only be called for actual files. Right now it's only called by the flush() family of + // functions, which require an updatable dictionary, so it's okay. But beware. + loadDictionary(dictFile.getAbsolutePath(), 0 /* startOffset */, + dictFile.length(), mIsUpdatable); } public void flush() { if (!isValidDictionary()) return; - flushNative(mNativeDict, mDictFilePath); - reopen(); + if (mHasUpdated) { + flushNative(mNativeDict, mDictFilePath); + reopen(); + } } public void flushWithGC() { @@ -333,6 +485,24 @@ public final class BinaryDictionary extends Dictionary { return needsToRunGCNative(mNativeDict, mindsBlockByGC); } + public boolean migrateTo(final int newFormatVersion) { + if (!isValidDictionary()) { + return false; + } + final String tmpDictFilePath = mDictFilePath + DICT_FILE_NAME_SUFFIX_FOR_MIGRATION; + // TODO: Implement migrateNative(tmpDictFilePath, newFormatVersion). + close(); + final File dictFile = new File(mDictFilePath); + final File tmpDictFile = new File(tmpDictFilePath); + FileUtils.deleteRecursively(dictFile); + if (!BinaryDictionaryUtils.renameDict(tmpDictFile, dictFile)) { + return false; + } + loadDictionary(dictFile.getAbsolutePath(), 0 /* startOffset */, + dictFile.length(), mIsUpdatable); + return true; + } + @UsedForTesting public int calculateProbability(final int unigramProbability, final int bigramProbability) { if (!isValidDictionary()) return NOT_A_PROBABILITY; @@ -340,7 +510,7 @@ public final class BinaryDictionary extends Dictionary { } @UsedForTesting - public String getPropertyForTests(String query) { + public String getPropertyForTest(final String query) { if (!isValidDictionary()) return ""; return getPropertyNative(mNativeDict, query); } diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index 722a82961..e428b1d54 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -98,7 +98,7 @@ public final class BinaryDictionaryFileDumper { * This creates a URI builder able to build a URI pointing to the dictionary * pack content provider for a specific dictionary id. */ - private static Uri.Builder getProviderUriBuilder(final String path) { + public static Uri.Builder getProviderUriBuilder(final String path) { return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) .authority(DictionaryPackConstants.AUTHORITY).appendPath(path); } @@ -142,7 +142,7 @@ public final class BinaryDictionaryFileDumper { final ContentProviderClient client = context.getContentResolver(). acquireContentProviderClient(getProviderUriBuilder("").build()); if (null == client) return Collections.<WordListInfo>emptyList(); - + Cursor cursor = null; try { final Uri.Builder builder = getContentUriBuilderForType(clientId, client, QUERY_PATH_DICT_INFO, locale.toString()); @@ -154,24 +154,22 @@ public final class BinaryDictionaryFileDumper { final boolean isProtocolV2 = (QUERY_PARAMETER_PROTOCOL_VALUE.equals( queryUri.getQueryParameter(QUERY_PARAMETER_PROTOCOL))); - Cursor c = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null); - if (isProtocolV2 && null == c) { + cursor = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null); + if (isProtocolV2 && null == cursor) { reinitializeClientRecordInDictionaryContentProvider(context, client, clientId); - c = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null); + cursor = client.query(queryUri, DICTIONARY_PROJECTION, null, null, null); } - if (null == c) return Collections.<WordListInfo>emptyList(); - if (c.getCount() <= 0 || !c.moveToFirst()) { - c.close(); + if (null == cursor) return Collections.<WordListInfo>emptyList(); + if (cursor.getCount() <= 0 || !cursor.moveToFirst()) { return Collections.<WordListInfo>emptyList(); } final ArrayList<WordListInfo> list = CollectionUtils.newArrayList(); do { - final String wordListId = c.getString(0); - final String wordListLocale = c.getString(1); + final String wordListId = cursor.getString(0); + final String wordListLocale = cursor.getString(1); if (TextUtils.isEmpty(wordListId)) continue; list.add(new WordListInfo(wordListId, wordListLocale)); - } while (c.moveToNext()); - c.close(); + } while (cursor.moveToNext()); return list; } catch (RemoteException e) { // The documentation is unclear as to in which cases this may happen, but it probably @@ -186,6 +184,9 @@ public final class BinaryDictionaryFileDumper { Log.e(TAG, "Unexpected exception communicating with the dictionary pack", e); return Collections.<WordListInfo>emptyList(); } finally { + if (null != cursor) { + cursor.close(); + } client.release(); } } @@ -339,15 +340,25 @@ public final class BinaryDictionaryFileDumper { Log.e(TAG, "Could not copy a word list. Will not be able to use it."); // If we can't copy it we should warn the dictionary provider so that it can mark it // as invalid. - wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, - QUERY_PARAMETER_FAILURE); + reportBrokenFileToDictionaryProvider(providerClient, clientId, wordlistId); + } + + public static boolean reportBrokenFileToDictionaryProvider( + final ContentProviderClient providerClient, final String clientId, + final String wordlistId) { try { + final Uri.Builder wordListUriBuilder = getContentUriBuilderForType(clientId, + providerClient, QUERY_PATH_DATAFILE, wordlistId /* extraPath */); + wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, + QUERY_PARAMETER_FAILURE); if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) { - Log.e(TAG, "In addition, we were unable to delete it."); + Log.e(TAG, "Unable to delete a word list."); } } catch (RemoteException e) { - Log.e(TAG, "In addition, communication with the dictionary provider was cut", e); + Log.e(TAG, "Communication with the dictionary provider was cut", e); + return false; } + return true; } // Ideally the two following methods should be merged, but AssetFileDescriptor does not @@ -432,8 +443,9 @@ public final class BinaryDictionaryFileDumper { // Actually copy the file final byte[] buffer = new byte[FILE_READ_BUFFER_SIZE]; - for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer)) + for (int readBytes = input.read(buffer); readBytes >= 0; readBytes = input.read(buffer)) { output.write(buffer, 0, readBytes); + } input.close(); } @@ -478,8 +490,7 @@ public final class BinaryDictionaryFileDumper { * @param context the context for resources and providers. * @param clientId the client ID to use. */ - public static void initializeClientRecordHelper(final Context context, - final String clientId) { + public static void initializeClientRecordHelper(final Context context, final String clientId) { try { final ContentProviderClient client = context.getContentResolver(). acquireContentProviderClient(getProviderUriBuilder("").build()); diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index 181ad17ea..4c49cb31c 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -21,10 +21,9 @@ import android.content.SharedPreferences; import android.content.res.AssetFileDescriptor; import android.util.Log; -import com.android.inputmethod.latin.makedict.DictDecoder; -import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.makedict.UnsupportedFormatException; +import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import com.android.inputmethod.latin.utils.LocaleUtils; @@ -112,7 +111,7 @@ final public class BinaryDictionaryGetter { public DictPackSettings(final Context context) { mDictPreferences = null == context ? null : context.getSharedPreferences(COMMON_PREFERENCES_NAME, - Context.MODE_WORLD_READABLE | Context.MODE_MULTI_PROCESS); + Context.MODE_MULTI_PROCESS); } public boolean isWordListActive(final String dictId) { if (null == mDictPreferences) { @@ -226,12 +225,10 @@ final public class BinaryDictionaryGetter { // ## HACK ## we prevent usage of a dictionary before version 18. The reason for this is, since // those do not include whitelist entries, the new code with an old version of the dictionary // would lose whitelist functionality. - private static boolean hackCanUseDictionaryFile(final Locale locale, final File f) { + private static boolean hackCanUseDictionaryFile(final Locale locale, final File file) { try { // Read the version of the file - final DictDecoder dictDecoder = FormatSpec.getDictDecoder(f); - final FileHeader header = dictDecoder.readHeader(); - + final DictionaryHeader header = BinaryDictionaryUtils.getHeader(file); final String version = header.mDictionaryOptions.mAttributes.get(VERSION_KEY); if (null == version) { // No version in the options : the format is unexpected diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 9a9653094..e71723a15 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -70,38 +70,47 @@ public final class Constants { public static final class ExtraValue { /** - * The subtype extra value used to indicate that the subtype keyboard layout is capable - * for typing ASCII characters. + * The subtype extra value used to indicate that this subtype is capable of + * entering ASCII characters. */ public static final String ASCII_CAPABLE = "AsciiCapable"; /** - * The subtype extra value used to indicate that the subtype keyboard layout is capable - * for typing EMOJI characters. + * The subtype extra value used to indicate that this subtype is enabled + * when the default subtype is not marked as ascii capable. + */ + public static final String ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE = + "EnabledWhenDefaultIsNotAsciiCapable"; + + /** + * The subtype extra value used to indicate that this subtype is capable of + * entering emoji characters. */ public static final String EMOJI_CAPABLE = "EmojiCapable"; + /** - * The subtype extra value used to indicate that the subtype require network connection - * to work. + * The subtype extra value used to indicate that this subtype requires a network + * connection to work. */ public static final String REQ_NETWORK_CONNECTIVITY = "requireNetworkConnectivity"; /** - * The subtype extra value used to indicate that the subtype display name contains "%s" - * for replacement mark and it should be replaced by this extra value. + * The subtype extra value used to indicate that the display name of this subtype + * contains a "%s" for printf-like replacement and it should be replaced by + * this extra value. * This extra value is supported on JellyBean and later. */ public static final String UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME = "UntranslatableReplacementStringInSubtypeName"; /** - * The subtype extra value used to indicate that the subtype keyboard layout set name. + * The subtype extra value used to indicate this subtype keyboard layout set name. * This extra value is private to LatinIME. */ public static final String KEYBOARD_LAYOUT_SET = "KeyboardLayoutSet"; /** - * The subtype extra value used to indicate that the subtype is additional subtype + * The subtype extra value used to indicate that this subtype is an additional subtype * that the user defined. This extra value is private to LatinIME. */ public static final String IS_ADDITIONAL_SUBTYPE = "isAdditionalSubtype"; @@ -124,6 +133,8 @@ public final class Constants { * {@link android.text.TextUtils#CAP_MODE_WORDS}, and * {@link android.text.TextUtils#CAP_MODE_SENTENCES}. */ + // TODO: Straighten this out. It's bizarre to have to use android.text.TextUtils.CAP_MODE_* + // except for OFF that is in Constants.TextUtils. public static final int CAP_MODE_OFF = 0; private TextUtils() { @@ -132,7 +143,8 @@ public final class Constants { } public static final int NOT_A_CODE = -1; - + public static final int NOT_A_CURSOR_POSITION = -1; + // TODO: replace the following constants with state in InputTransaction? public static final int NOT_A_COORDINATE = -1; public static final int SUGGESTION_STRIP_COORDINATE = -2; public static final int SPELL_CHECKER_COORDINATE = -3; @@ -145,6 +157,13 @@ public final class Constants { // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h public static final int DICTIONARY_MAX_WORD_LENGTH = 48; + // Key events coming any faster than this are long-presses. + public static final int LONG_PRESS_MILLISECONDS = 200; + // TODO: Set this value appropriately. + public static final int GET_SUGGESTED_WORDS_TIMEOUT = 200; + // How many continuous deletes at which to start deleting at a higher speed. + public static final int DELETE_ACCELERATE_AT = 20; + public static boolean isValidCoordinate(final int coordinate) { // Detect {@link NOT_A_COORDINATE}, {@link SUGGESTION_STRIP_COORDINATE}, // and {@link SPELL_CHECKER_COORDINATE}. @@ -165,6 +184,7 @@ public final class Constants { public static final int CODE_TAB = '\t'; public static final int CODE_SPACE = ' '; public static final int CODE_PERIOD = '.'; + public static final int CODE_COMMA = ','; public static final int CODE_ARMENIAN_PERIOD = 0x0589; public static final int CODE_DASH = '-'; public static final int CODE_SINGLE_QUOTE = '\''; @@ -172,6 +192,8 @@ public final class Constants { public static final int CODE_QUESTION_MARK = '?'; public static final int CODE_EXCLAMATION_MARK = '!'; public static final int CODE_SLASH = '/'; + public static final int CODE_BACKSLASH = '\\'; + public static final int CODE_VERTICAL_BAR = '|'; public static final int CODE_COMMERCIAL_AT = '@'; public static final int CODE_PLUS = '+'; public static final int CODE_PERCENT = '%'; @@ -197,8 +219,10 @@ public final class Constants { public static final int CODE_LANGUAGE_SWITCH = -10; public static final int CODE_EMOJI = -11; public static final int CODE_SHIFT_ENTER = -12; + public static final int CODE_SYMBOL_SHIFT = -13; + public static final int CODE_ALPHA_FROM_EMOJI = -14; // Code value representing the code is not specified. - public static final int CODE_UNSPECIFIED = -13; + public static final int CODE_UNSPECIFIED = -15; public static boolean isLetterCode(final int code) { return code >= CODE_SPACE; @@ -221,6 +245,7 @@ public final class Constants { case CODE_UNSPECIFIED: return "unspec"; case CODE_TAB: return "tab"; case CODE_ENTER: return "enter"; + case CODE_ALPHA_FROM_EMOJI: return "alpha"; default: if (code < CODE_SPACE) return String.format("'\\u%02x'", code); if (code < 0x100) return String.format("'%c'", code); @@ -228,10 +253,37 @@ public final class Constants { } } + public static String printableCodes(final int[] codes) { + final StringBuilder sb = new StringBuilder(); + boolean addDelimiter = false; + for (final int code : codes) { + if (code == NOT_A_CODE) break; + if (addDelimiter) sb.append(", "); + sb.append(printableCode(code)); + addDelimiter = true; + } + return "[" + sb + "]"; + } + public static final int MAX_INT_BIT_COUNT = 32; + /** + * Screen metrics (a.k.a. Device form factor) constants of + * {@link R.integer#config_screen_metrics}. + */ + public static final int SCREEN_METRICS_SMALL_PHONE = 0; + public static final int SCREEN_METRICS_LARGE_PHONE = 1; + public static final int SCREEN_METRICS_LARGE_TABLET = 2; + public static final int SCREEN_METRICS_SMALL_TABLET = 3; + + /** + * Default capacity of gesture points container. + * This constant is used by {@link BatchInputArbiter} and etc. to preallocate regions that + * contain gesture event points. + */ + public static final int DEFAULT_GESTURE_POINTS_CAPACITY = 128; + private Constants() { // This utility class is not publicly instantiable. } - } diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java index 47891c6b7..d5873d70f 100644 --- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java @@ -16,8 +16,6 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.latin.personalization.AccountUtils; - import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -31,8 +29,10 @@ import android.provider.ContactsContract.Contacts; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.latin.personalization.AccountUtils; import com.android.inputmethod.latin.utils.StringUtils; +import java.io.File; import java.util.List; import java.util.Locale; @@ -44,7 +44,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { private static final String TAG = ContactsBinaryDictionary.class.getSimpleName(); private static final String NAME = "contacts"; - private static boolean DEBUG = false; + private static final boolean DEBUG = false; + private static final boolean DEBUG_DUMP = false; /** * Frequency for contacts information into the dictionary @@ -71,8 +72,13 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { private final boolean mUseFirstLastBigrams; public ContactsBinaryDictionary(final Context context, final Locale locale) { - super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS, - false /* isUpdatable */); + this(context, locale, null /* dictFile */); + } + + public ContactsBinaryDictionary(final Context context, final Locale locale, + final File dictFile) { + super(context, getDictName(NAME, locale, dictFile), locale, Dictionary.TYPE_CONTACTS, + dictFile); mLocale = locale; mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale); registerObserver(context); @@ -83,8 +89,6 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { } private synchronized void registerObserver(final Context context) { - // Perform a managed query. The Activity will handle closing and requerying the cursor - // when needed. if (mObserver != null) return; ContentResolver cres = context.getContentResolver(); cres.registerContentObserver(Contacts.CONTENT_URI, true, mObserver = @@ -96,10 +100,6 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { }); } - public void reopen(final Context context) { - registerObserver(context); - } - @Override public synchronized void close() { if (mObserver != null) { @@ -110,14 +110,14 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { } @Override - public void loadDictionaryAsync() { - loadDeviceAccountsEmailAddresses(); - loadDictionaryAsyncForUri(ContactsContract.Profile.CONTENT_URI); + public void loadInitialContentsLocked() { + loadDeviceAccountsEmailAddressesLocked(); + loadDictionaryForUriLocked(ContactsContract.Profile.CONTENT_URI); // TODO: Switch this URL to the newer ContactsContract too - loadDictionaryAsyncForUri(Contacts.CONTENT_URI); + loadDictionaryForUriLocked(Contacts.CONTENT_URI); } - private void loadDeviceAccountsEmailAddresses() { + private void loadDeviceAccountsEmailAddressesLocked() { final List<String> accountVocabulary = AccountUtils.getDeviceAccountsEmailAddresses(mContext); if (accountVocabulary == null || accountVocabulary.isEmpty()) { @@ -127,29 +127,32 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { if (DEBUG) { Log.d(TAG, "loadAccountVocabulary: " + word); } - super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS, 0 /* shortcutFreq */, - false /* isNotAWord */); + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addWordDynamicallyLocked(word, FREQUENCY_FOR_CONTACTS, null /* shortcut */, + 0 /* shortcutFreq */, false /* isNotAWord */, false /* isBlacklisted */, + BinaryDictionary.NOT_A_VALID_TIMESTAMP); } } - private void loadDictionaryAsyncForUri(final Uri uri) { + private void loadDictionaryForUriLocked(final Uri uri) { + Cursor cursor = null; try { - Cursor cursor = mContext.getContentResolver() - .query(uri, PROJECTION, null, null, null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - sContactCountAtLastRebuild = getContactCount(); - addWords(cursor); - } - } finally { - cursor.close(); - } + cursor = mContext.getContentResolver().query(uri, PROJECTION, null, null, null); + if (null == cursor) { + return; + } + if (cursor.moveToFirst()) { + sContactCountAtLastRebuild = getContactCount(); + addWordsLocked(cursor); } } catch (final SQLiteException e) { Log.e(TAG, "SQLiteException in the remote Contacts process.", e); } catch (final IllegalStateException e) { Log.e(TAG, "Contacts DB is having problems", e); + } finally { + if (null != cursor) { + cursor.close(); + } } } @@ -161,13 +164,17 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { return false; } - private void addWords(final Cursor cursor) { + private void addWordsLocked(final Cursor cursor) { int count = 0; while (!cursor.isAfterLast() && count < MAX_CONTACT_COUNT) { String name = cursor.getString(INDEX_NAME); if (isValidName(name)) { - addName(name); + addNameLocked(name); ++count; + } else { + if (DEBUG_DUMP) { + Log.d(TAG, "Invalid name: " + name); + } } cursor.moveToNext(); } @@ -176,18 +183,20 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { private int getContactCount() { // TODO: consider switching to a rawQuery("select count(*)...") on the database if // performance is a bottleneck. + Cursor cursor = null; try { - final Cursor cursor = mContext.getContentResolver().query( - Contacts.CONTENT_URI, PROJECTION_ID_ONLY, null, null, null); - if (cursor != null) { - try { - return cursor.getCount(); - } finally { - cursor.close(); - } + cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI, PROJECTION_ID_ONLY, + null, null, null); + if (null == cursor) { + return 0; } + return cursor.getCount(); } catch (final SQLiteException e) { Log.e(TAG, "SQLiteException in the remote Contacts process.", e); + } finally { + if (null != cursor) { + cursor.close(); + } } return 0; } @@ -196,7 +205,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { * Adds the words in a name (e.g., firstname/lastname) to the binary dictionary along with their * bigrams depending on locale. */ - private void addName(final String name) { + private void addNameLocked(final String name) { int len = StringUtils.codePointCount(name); String prevWord = null; // TODO: Better tokenization for non-Latin writing systems @@ -204,6 +213,9 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { if (Character.isLetter(name.codePointAt(i))) { int end = getWordEndPosition(name, len, i); String word = name.substring(i, end); + if (DEBUG_DUMP) { + Log.d(TAG, "addName word = " + word); + } i = end - 1; // Don't add single letter words, possibly confuses // capitalization of i. @@ -212,13 +224,15 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { if (DEBUG) { Log.d(TAG, "addName " + name + ", " + word + ", " + prevWord); } - super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS, - 0 /* shortcutFreq */, false /* isNotAWord */); - if (!TextUtils.isEmpty(prevWord)) { - if (mUseFirstLastBigrams) { - super.addBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM, - 0 /* lastModifiedTime */); - } + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addWordDynamicallyLocked(word, FREQUENCY_FOR_CONTACTS, + null /* shortcut */, 0 /* shortcutFreq */, false /* isNotAWord */, + false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP); + if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) { + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addBigramDynamicallyLocked(prevWord, word, + FREQUENCY_FOR_CONTACTS_BIGRAM, + BinaryDictionary.NOT_A_VALID_TIMESTAMP); } prevWord = word; } @@ -244,12 +258,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { } @Override - protected boolean needsToReloadBeforeWriting() { - return true; - } - - @Override - protected boolean hasContentChanged() { + protected boolean haveContentsChanged() { final long startTime = SystemClock.uptimeMillis(); final int contactCount = getContactCount(); if (contactCount > MAX_CONTACT_COUNT) { @@ -268,26 +277,27 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { // Check all contacts since it's not possible to find out which names have changed. // This is needed because it's possible to receive extraneous onChange events even when no // name has changed. - Cursor cursor = mContext.getContentResolver().query( - Contacts.CONTENT_URI, PROJECTION, null, null, null); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - while (!cursor.isAfterLast()) { - String name = cursor.getString(INDEX_NAME); - if (isValidName(name) && !isNameInDictionary(name)) { - if (DEBUG) { - Log.d(TAG, "Contact name missing: " + name + " (runtime = " - + (SystemClock.uptimeMillis() - startTime) + " ms)"); - } - return true; + final Cursor cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI, PROJECTION, + null, null, null); + if (null == cursor) { + return false; + } + try { + if (cursor.moveToFirst()) { + while (!cursor.isAfterLast()) { + String name = cursor.getString(INDEX_NAME); + if (isValidName(name) && !isNameInDictionaryLocked(name)) { + if (DEBUG) { + Log.d(TAG, "Contact name missing: " + name + " (runtime = " + + (SystemClock.uptimeMillis() - startTime) + " ms)"); } - cursor.moveToNext(); + return true; } + cursor.moveToNext(); } - } finally { - cursor.close(); } + } finally { + cursor.close(); } if (DEBUG) { Log.d(TAG, "No contacts changed. (runtime = " + (SystemClock.uptimeMillis() - startTime) @@ -306,7 +316,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { /** * Checks if the words in a name are in the current binary dictionary. */ - private boolean isNameInDictionary(final String name) { + private boolean isNameInDictionaryLocked(final String name) { int len = StringUtils.codePointCount(name); String prevWord = null; for (int i = 0; i < len; i++) { @@ -317,11 +327,11 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { final int wordLen = StringUtils.codePointCount(word); if (wordLen < MAX_WORD_LENGTH && wordLen > 1) { if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) { - if (!super.isValidBigramLocked(prevWord, word)) { + if (!isValidBigramLocked(prevWord, word)) { return false; } } else { - if (!super.isValidWordLocked(word)) { + if (!isValidWordLocked(word)) { return false; } } diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java index fa79f5af7..0742fbde9 100644 --- a/java/src/com/android/inputmethod/latin/Dictionary.java +++ b/java/src/com/android/inputmethod/latin/Dictionary.java @@ -27,6 +27,7 @@ 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; // The following types do not actually come from real dictionary instances, so we create // corresponding instances. @@ -52,13 +53,10 @@ public abstract class Dictionary { public static final String TYPE_CONTACTS = "contacts"; // User dictionary, the system-managed one. public static final String TYPE_USER = "user"; - // User history dictionary internal to LatinIME. This assumes bigram prediction for now. + // User history dictionary internal to LatinIME. public static final String TYPE_USER_HISTORY = "history"; - // Personalization binary dictionary internal to LatinIME. + // Personalization dictionary. public static final String TYPE_PERSONALIZATION = "personalization"; - // Personalization prediction dictionary internal to LatinIME's Java code. - public static final String TYPE_PERSONALIZATION_PREDICTION_IN_JAVA = - "personalization_prediction_in_java"; public final String mDictType; public Dictionary(final String dictType) { @@ -73,22 +71,26 @@ public abstract class Dictionary { * @param proximityInfo the object for key proximity. May be ignored by some implementations. * @param blockOffensiveWords whether to block potentially offensive words * @param additionalFeaturesOptions options about additional features used for the suggestion. + * @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. * @return the list of suggestions (possibly null if none) */ // TODO: pass more context than just the previous word, to enable better suggestions (n-gram // and more) abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions); + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final float[] inOutLanguageWeight); // The default implementation of this method ignores sessionId. // Subclasses that want to use sessionId need to override this method. public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, - final int sessionId) { + final int sessionId, final float[] inOutLanguageWeight) { return getSuggestions(composer, prevWord, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions); + additionalFeaturesOptions, inOutLanguageWeight); } /** @@ -162,7 +164,8 @@ public abstract class Dictionary { @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final float[] inOutLanguageWeight) { return null; } diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java index bf075140e..16173fffc 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java +++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java @@ -58,18 +58,21 @@ public final class DictionaryCollection extends Dictionary { @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final float[] inOutLanguageWeight) { 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, - prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions); + prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions, + inOutLanguageWeight); if (null == suggestions) suggestions = CollectionUtils.newArrayList(); final int length = dictionaries.size(); for (int i = 1; i < length; ++ i) { final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer, - prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions); + prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions, + inOutLanguageWeight); if (null != sugg) suggestions.addAll(sugg); } return suggestions; diff --git a/java/src/com/android/inputmethod/latin/DictionaryDumpBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/DictionaryDumpBroadcastReceiver.java new file mode 100644 index 000000000..ee2fdc6c7 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryDumpBroadcastReceiver.java @@ -0,0 +1,50 @@ +/* + * 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.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class DictionaryDumpBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = DictionaryDumpBroadcastReceiver.class.getSimpleName(); + + private static final String DOMAIN = "com.android.inputmethod.latin"; + public static final String DICTIONARY_DUMP_INTENT_ACTION = DOMAIN + ".DICT_DUMP"; + public static final String DICTIONARY_NAME_KEY = "dictName"; + + final LatinIME mLatinIme; + + public DictionaryDumpBroadcastReceiver(final LatinIME latinIme) { + mLatinIme = latinIme; + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action.equals(DICTIONARY_DUMP_INTENT_ACTION)) { + final String dictName = intent.getStringExtra(DICTIONARY_NAME_KEY); + if (dictName == null) { + Log.e(TAG, "Received dictionary dump intent action " + + "but the dictionary name is not set."); + return; + } + mLatinIme.dumpDictionaryForDebug(dictName); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java new file mode 100644 index 000000000..0b6258a7f --- /dev/null +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java @@ -0,0 +1,593 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.personalization.PersonalizationDictionary; +import com.android.inputmethod.latin.personalization.PersonalizationHelper; +import com.android.inputmethod.latin.personalization.UserHistoryDictionary; +import com.android.inputmethod.latin.utils.CollectionUtils; +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; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +// TODO: Consolidate dictionaries in native code. +public class DictionaryFacilitatorForSuggest { + public static final String TAG = DictionaryFacilitatorForSuggest.class.getSimpleName(); + + // HACK: This threshold is being used when adding a capitalized entry in the User History + // dictionary. + private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140; + + private Dictionaries mDictionaries = new Dictionaries(); + private volatile CountDownLatch mLatchForWaitingLoadingMainDictionary = new CountDownLatch(0); + // To synchronize assigning mDictionaries to ensure closing dictionaries. + private Object mLock = new Object(); + + private static final String[] dictTypesOrderedToGetSuggestion = + new String[] { + Dictionary.TYPE_MAIN, + Dictionary.TYPE_USER_HISTORY, + Dictionary.TYPE_PERSONALIZATION, + Dictionary.TYPE_USER, + Dictionary.TYPE_CONTACTS + }; + + /** + * Class contains dictionaries for a locale. + */ + private static class Dictionaries { + public final Locale mLocale; + public final ConcurrentHashMap<String, Dictionary> mDictMap = + CollectionUtils.newConcurrentHashMap(); + // Main dictionary will be asynchronously loaded. + public Dictionary mMainDictionary; + public final ContactsBinaryDictionary mContactsDictionary; + public final UserBinaryDictionary mUserDictionary; + public final UserHistoryDictionary mUserHistoryDictionary; + public final PersonalizationDictionary mPersonalizationDictionary; + + public Dictionaries() { + mLocale = null; + mMainDictionary = null; + mContactsDictionary = null; + mUserDictionary = null; + mUserHistoryDictionary = null; + mPersonalizationDictionary = null; + } + + public Dictionaries(final Locale locale, final Dictionary mainDict, + final ContactsBinaryDictionary contactsDict, final UserBinaryDictionary userDict, + final UserHistoryDictionary userHistoryDict, + final PersonalizationDictionary personalizationDict) { + mLocale = locale; + setMainDict(mainDict); + mContactsDictionary = contactsDict; + if (mContactsDictionary != null) { + mDictMap.put(Dictionary.TYPE_CONTACTS, mContactsDictionary); + } + mUserDictionary = userDict; + if (mUserDictionary != null) { + mDictMap.put(Dictionary.TYPE_USER, mUserDictionary); + } + mUserHistoryDictionary = userHistoryDict; + if (mUserHistoryDictionary != null) { + mDictMap.put(Dictionary.TYPE_USER_HISTORY, mUserHistoryDictionary); + } + mPersonalizationDictionary = personalizationDict; + if (mPersonalizationDictionary != null) { + mDictMap.put(Dictionary.TYPE_PERSONALIZATION, mPersonalizationDictionary); + } + } + + public void setMainDict(final Dictionary mainDict) { + mMainDictionary = mainDict; + // Close old dictionary if exists. Main dictionary can be assigned multiple times. + final Dictionary oldDict; + if (mMainDictionary != null) { + oldDict = mDictMap.put(Dictionary.TYPE_MAIN, mMainDictionary); + } else { + oldDict = mDictMap.remove(Dictionary.TYPE_MAIN); + } + if (oldDict != null && mMainDictionary != oldDict) { + oldDict.close(); + } + } + + public boolean hasMainDict() { + return mMainDictionary != null; + } + + public boolean hasContactsDict() { + return mContactsDictionary != null; + } + + public boolean hasUserDict() { + return mUserDictionary != null; + } + + public boolean hasUserHistoryDict() { + return mUserHistoryDictionary != null; + } + + public boolean hasPersonalizationDict() { + return mPersonalizationDictionary != null; + } + } + + public interface DictionaryInitializationListener { + public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); + } + + public DictionaryFacilitatorForSuggest() {} + + public Locale getLocale() { + return mDictionaries.mLocale; + } + + public void resetDictionaries(final Context context, final Locale newLocale, + final boolean useContactsDict, final boolean usePersonalizedDicts, + final boolean forceReloadMainDictionary, + final DictionaryInitializationListener listener) { + 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 boolean closeContactsDictionary = localeHasBeenChanged || !useContactsDict; + final boolean closeUserDictionary = localeHasBeenChanged; + final boolean closeUserHistoryDictionary = localeHasBeenChanged || !usePersonalizedDicts; + final boolean closePersonalizationDictionary = + localeHasBeenChanged || !usePersonalizedDicts; + + final Dictionary newMainDict; + if (reloadMainDictionary) { + // The main dictionary will be asynchronously loaded. + newMainDict = null; + } else { + newMainDict = mDictionaries.mMainDictionary; + } + + // Open or move contacts dictionary. + final ContactsBinaryDictionary newContactsDict; + if (!closeContactsDictionary && mDictionaries.hasContactsDict()) { + newContactsDict = mDictionaries.mContactsDictionary; + } else if (useContactsDict) { + newContactsDict = new ContactsBinaryDictionary(context, newLocale); + } else { + newContactsDict = null; + } + + // Open or move user dictionary. + final UserBinaryDictionary newUserDictionary; + if (!closeUserDictionary && mDictionaries.hasUserDict()) { + newUserDictionary = mDictionaries.mUserDictionary; + } else { + newUserDictionary = new UserBinaryDictionary(context, newLocale); + } + + // Open or move user history dictionary. + final UserHistoryDictionary newUserHistoryDict; + if (!closeUserHistoryDictionary && mDictionaries.hasUserHistoryDict()) { + newUserHistoryDict = mDictionaries.mUserHistoryDictionary; + } else if (usePersonalizedDicts) { + newUserHistoryDict = PersonalizationHelper.getUserHistoryDictionary(context, newLocale); + } else { + newUserHistoryDict = null; + } + + // Open or move personalization dictionary. + final PersonalizationDictionary newPersonalizationDict; + if (!closePersonalizationDictionary && mDictionaries.hasPersonalizationDict()) { + newPersonalizationDict = mDictionaries.mPersonalizationDictionary; + } else if (usePersonalizedDicts) { + newPersonalizationDict = + PersonalizationHelper.getPersonalizationDictionary(context, newLocale); + } else { + newPersonalizationDict = null; + } + + // Replace Dictionaries. + final Dictionaries newDictionaries = new Dictionaries(newLocale, newMainDict, + newContactsDict, newUserDictionary, newUserHistoryDict, newPersonalizationDict); + if (listener != null) { + listener.onUpdateMainDictionaryAvailability(newDictionaries.hasMainDict()); + } + final Dictionaries oldDictionaries; + synchronized (mLock) { + oldDictionaries = mDictionaries; + mDictionaries = newDictionaries; + if (reloadMainDictionary) { + asyncReloadMainDictionary(context, newLocale, listener); + } + } + + // Clean up old dictionaries. + oldDictionaries.mDictMap.clear(); + if (reloadMainDictionary && oldDictionaries.hasMainDict()) { + oldDictionaries.mMainDictionary.close(); + } + if (closeContactsDictionary && oldDictionaries.hasContactsDict()) { + oldDictionaries.mContactsDictionary.close(); + } + if (closeUserDictionary && oldDictionaries.hasUserDict()) { + oldDictionaries.mUserDictionary.close(); + } + if (closeUserHistoryDictionary && oldDictionaries.hasUserHistoryDict()) { + oldDictionaries.mUserHistoryDictionary.close(); + } + if (closePersonalizationDictionary && oldDictionaries.hasPersonalizationDict()) { + oldDictionaries.mPersonalizationDictionary.close(); + } + } + + private void asyncReloadMainDictionary(final Context context, final Locale locale, + final DictionaryInitializationListener listener) { + final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); + mLatchForWaitingLoadingMainDictionary = 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(); + } + } + if (listener != null) { + listener.onUpdateMainDictionaryAvailability(mDictionaries.hasMainDict()); + } + latchForWaitingLoadingMainDictionary.countDown(); + } + }); + } + + @UsedForTesting + public void resetDictionariesForTesting(final Context context, final Locale locale, + final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles, + final Map<String, Map<String, String>> additionalDictAttributes) { + Dictionary mainDictionary = null; + ContactsBinaryDictionary contactsDictionary = null; + UserBinaryDictionary userDictionary = null; + UserHistoryDictionary userHistoryDictionary = null; + PersonalizationDictionary personalizationDictionary = null; + + for (final String dictType : dictionaryTypes) { + if (dictType.equals(Dictionary.TYPE_MAIN)) { + mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context, locale); + } else if (dictType.equals(Dictionary.TYPE_USER_HISTORY)) { + userHistoryDictionary = + PersonalizationHelper.getUserHistoryDictionary(context, locale); + // Staring with an empty user history dictionary for testing. + // Testing program may populate this dictionary before actual testing. + userHistoryDictionary.reloadDictionaryIfRequired(); + userHistoryDictionary.waitAllTasksForTests(); + if (additionalDictAttributes.containsKey(dictType)) { + userHistoryDictionary.clearAndFlushDictionaryWithAdditionalAttributes( + additionalDictAttributes.get(dictType)); + } + } else if (dictType.equals(Dictionary.TYPE_PERSONALIZATION)) { + personalizationDictionary = + PersonalizationHelper.getPersonalizationDictionary(context, locale); + // Staring with an empty personalization dictionary for testing. + // Testing program may populate this dictionary before actual testing. + personalizationDictionary.reloadDictionaryIfRequired(); + personalizationDictionary.waitAllTasksForTests(); + if (additionalDictAttributes.containsKey(dictType)) { + personalizationDictionary.clearAndFlushDictionaryWithAdditionalAttributes( + additionalDictAttributes.get(dictType)); + } + } else if (dictType.equals(Dictionary.TYPE_USER)) { + final File file = dictionaryFiles.get(dictType); + userDictionary = new UserBinaryDictionary(context, locale, file); + userDictionary.reloadDictionaryIfRequired(); + userDictionary.waitAllTasksForTests(); + } else if (dictType.equals(Dictionary.TYPE_CONTACTS)) { + final File file = dictionaryFiles.get(dictType); + contactsDictionary = new ContactsBinaryDictionary(context, locale, file); + contactsDictionary.reloadDictionaryIfRequired(); + contactsDictionary.waitAllTasksForTests(); + } else { + throw new RuntimeException("Unknown dictionary type: " + dictType); + } + } + mDictionaries = new Dictionaries(locale, mainDictionary, contactsDictionary, + userDictionary, userHistoryDictionary, personalizationDictionary); + } + + public void closeDictionaries() { + final Dictionaries dictionaries; + synchronized (mLock) { + dictionaries = mDictionaries; + mDictionaries = new Dictionaries(); + } + if (dictionaries.hasMainDict()) { + dictionaries.mMainDictionary.close(); + } + if (dictionaries.hasContactsDict()) { + dictionaries.mContactsDictionary.close(); + } + if (dictionaries.hasUserDict()) { + dictionaries.mUserDictionary.close(); + } + if (dictionaries.hasUserHistoryDict()) { + dictionaries.mUserHistoryDictionary.close(); + } + if (dictionaries.hasPersonalizationDict()) { + dictionaries.mPersonalizationDictionary.close(); + } + } + + // The main dictionary could have been loaded asynchronously. Don't cache the return value + // of this method. + public boolean hasInitializedMainDictionary() { + final Dictionaries dictionaries = mDictionaries; + return dictionaries.hasMainDict() && dictionaries.mMainDictionary.isInitialized(); + } + + public boolean hasPersonalizationDictionary() { + return mDictionaries.hasPersonalizationDict(); + } + + public void flushPersonalizationDictionary() { + final PersonalizationDictionary personalizationDict = + mDictionaries.mPersonalizationDictionary; + if (personalizationDict != null) { + personalizationDict.flush(); + } + } + + public void waitForLoadingMainDictionary(final long timeout, final TimeUnit unit) + throws InterruptedException { + mLatchForWaitingLoadingMainDictionary.await(timeout, unit); + } + + @UsedForTesting + public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) + throws InterruptedException { + waitForLoadingMainDictionary(timeout, unit); + final Dictionaries dictionaries = mDictionaries; + if (dictionaries.hasContactsDict()) { + dictionaries.mContactsDictionary.waitAllTasksForTests(); + } + if (dictionaries.hasUserDict()) { + dictionaries.mUserDictionary.waitAllTasksForTests(); + } + if (dictionaries.hasUserHistoryDict()) { + dictionaries.mUserHistoryDictionary.waitAllTasksForTests(); + } + if (dictionaries.hasPersonalizationDict()) { + dictionaries.mPersonalizationDictionary.waitAllTasksForTests(); + } + } + + public boolean isUserDictionaryEnabled() { + final UserBinaryDictionary userDictionary = mDictionaries.mUserDictionary; + if (userDictionary == null) { + return false; + } + return userDictionary.mEnabled; + } + + public void addWordToUserDictionary(String word) { + final UserBinaryDictionary userDictionary = mDictionaries.mUserDictionary; + if (userDictionary == null) { + return; + } + userDictionary.addWordToUserDictionary(word); + } + + public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, + final String previousWord, final int timeStampInSeconds) { + final Dictionaries dictionaries = mDictionaries; + if (!dictionaries.hasUserHistoryDict()) { + return; + } + final int maxFreq = getMaxFrequency(suggestion); + if (maxFreq == 0) { + return; + } + final String suggestionLowerCase = suggestion.toLowerCase(dictionaries.mLocale); + final String secondWord; + if (wasAutoCapitalized) { + if (isValidWord(suggestion, false /* ignoreCase */) + && !isValidWord(suggestionLowerCase, false /* ignoreCase */)) { + // If the word was auto-capitalized and exists only as a capitalized word in the + // dictionary, then we must not downcase it before registering it. For example, + // the name of the contacts in start-of-sentence position would come here with the + // wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version + // of that contact's name which would end up popping in suggestions. + secondWord = suggestion; + } else { + // If however the word is not in the dictionary, or exists as a lower-case word + // only, then we consider that was a lower-case word that had been auto-capitalized. + secondWord = suggestionLowerCase; + } + } else { + // HACK: We'd like to avoid adding the capitalized form of common words to the User + // History dictionary in order to avoid suggesting them until the dictionary + // consolidation is done. + // TODO: Remove this hack when ready. + final int lowerCaseFreqInMainDict = dictionaries.hasMainDict() ? + dictionaries.mMainDictionary.getFrequency(suggestionLowerCase) : + Dictionary.NOT_A_PROBABILITY; + if (maxFreq < lowerCaseFreqInMainDict + && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) { + // Use lower cased word as the word can be a distracter of the popular word. + secondWord = suggestionLowerCase; + } else { + secondWord = suggestion; + } + } + // 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; + dictionaries.mUserHistoryDictionary.addToDictionary( + previousWord, secondWord, isValid, timeStampInSeconds); + } + + public void cancelAddingUserHistory(final String previousWord, final String committedWord) { + final UserHistoryDictionary userHistoryDictionary = mDictionaries.mUserHistoryDictionary; + if (userHistoryDictionary != null) { + userHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord); + } + } + + // TODO: Revise the way to fusion suggestion results. + public SuggestionResults getSuggestionResults(final WordComposer composer, + final String prevWord, final ProximityInfo proximityInfo, + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final int sessionId, final ArrayList<SuggestedWordInfo> rawSuggestions) { + final Dictionaries dictionaries = mDictionaries; + final Map<String, Dictionary> dictMap = dictionaries.mDictMap; + final SuggestionResults suggestionResults = + new SuggestionResults(dictionaries.mLocale, SuggestedWords.MAX_SUGGESTIONS); + final float[] languageWeight = new float[] { Dictionary.NOT_A_LANGUAGE_WEIGHT }; + for (final String dictType : dictTypesOrderedToGetSuggestion) { + final Dictionary dictionary = dictMap.get(dictType); + if (null == dictionary) continue; + final ArrayList<SuggestedWordInfo> dictionarySuggestions = + dictionary.getSuggestionsWithSessionId(composer, prevWord, proximityInfo, + blockOffensiveWords, additionalFeaturesOptions, sessionId, + languageWeight); + if (null == dictionarySuggestions) continue; + suggestionResults.addAll(dictionarySuggestions); + if (null != rawSuggestions) { + rawSuggestions.addAll(dictionarySuggestions); + } + } + return suggestionResults; + } + + public boolean isValidMainDictWord(final String word) { + final Dictionaries dictionaries = mDictionaries; + if (TextUtils.isEmpty(word) || !dictionaries.hasMainDict()) { + return false; + } + return dictionaries.mMainDictionary.isValidWord(word); + } + + public boolean isValidWord(final String word, final boolean ignoreCase) { + if (TextUtils.isEmpty(word)) { + return false; + } + final Dictionaries dictionaries = mDictionaries; + if (dictionaries.mLocale == null) { + return false; + } + final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); + final Map<String, Dictionary> dictMap = dictionaries.mDictMap; + for (final Dictionary dictionary : dictMap.values()) { + // 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; + } + + private int getMaxFrequency(final String word) { + if (TextUtils.isEmpty(word)) { + return Dictionary.NOT_A_PROBABILITY; + } + int maxFreq = -1; + final Map<String, Dictionary> dictMap = mDictionaries.mDictMap; + for (final Dictionary dictionary : dictMap.values()) { + final int tempFreq = dictionary.getFrequency(word); + if (tempFreq >= maxFreq) { + maxFreq = tempFreq; + } + } + return maxFreq; + } + + + public void clearUserHistoryDictionary() { + final UserHistoryDictionary userHistoryDict = mDictionaries.mUserHistoryDictionary; + if (userHistoryDict == null) { + return; + } + userHistoryDict.clearAndFlushDictionary(); + } + + // This method gets called only when the IME receives a notification to remove the + // personalization dictionary. + public void clearPersonalizationDictionary() { + final PersonalizationDictionary personalizationDict = + mDictionaries.mPersonalizationDictionary; + if (personalizationDict == null) { + return; + } + personalizationDict.clearAndFlushDictionary(); + } + + public void addMultipleDictionaryEntriesToPersonalizationDictionary( + final ArrayList<LanguageModelParam> languageModelParams, + final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) { + final PersonalizationDictionary personalizationDict = + mDictionaries.mPersonalizationDictionary; + if (personalizationDict == null) { + if (callback != null) { + callback.onFinished(); + } + return; + } + personalizationDict.addMultipleDictionaryEntriesToDictionary(languageModelParams, callback); + } + + public void dumpDictionaryForDebug(final String dictName) { + final ExpandableBinaryDictionary dictToDump; + if (dictName.equals(Dictionary.TYPE_CONTACTS)) { + dictToDump = mDictionaries.mContactsDictionary; + } else if (dictName.equals(Dictionary.TYPE_USER)) { + dictToDump = mDictionaries.mUserDictionary; + } else if (dictName.equals(Dictionary.TYPE_USER_HISTORY)) { + dictToDump = mDictionaries.mUserHistoryDictionary; + } else if (dictName.equals(Dictionary.TYPE_PERSONALIZATION)) { + dictToDump = mDictionaries.mPersonalizationDictionary; + } else { + dictToDump = null; + } + if (dictToDump == null) { + Log.e(TAG, "Cannot dump " + dictName + ". " + + "The dictionary is not being used for suggestion or cannot be dumped."); + return; + } + dictToDump.dumpAllWordsForDebug(); + } +} diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java index 828e54f14..e09c309ea 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin; +import android.content.ContentProviderClient; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; @@ -64,6 +65,10 @@ public final class DictionaryFactory { useFullEditDistance, locale, Dictionary.TYPE_MAIN); if (readOnlyBinaryDictionary.isValidDictionary()) { dictList.add(readOnlyBinaryDictionary); + } else { + readOnlyBinaryDictionary.close(); + // Prevent this dictionary to do any further harm. + killDictionary(context, f); } } } @@ -75,6 +80,51 @@ public final class DictionaryFactory { } /** + * Kills a dictionary so that it is never used again, if possible. + * @param context The context to contact the dictionary provider, if possible. + * @param f A file address to the dictionary to kill. + */ + private static void killDictionary(final Context context, final AssetFileAddress f) { + if (f.pointsToPhysicalFile()) { + f.deleteUnderlyingFile(); + // Warn the dictionary provider if the dictionary came from there. + final ContentProviderClient providerClient; + try { + providerClient = context.getContentResolver().acquireContentProviderClient( + BinaryDictionaryFileDumper.getProviderUriBuilder("").build()); + } catch (final SecurityException e) { + Log.e(TAG, "No permission to communicate with the dictionary provider", e); + return; + } + if (null == providerClient) { + Log.e(TAG, "Can't establish communication with the dictionary provider"); + return; + } + final String wordlistId = + DictionaryInfoUtils.getWordListIdFromFileName(new File(f.mFilename).getName()); + if (null != wordlistId) { + // TODO: this is a reasonable last resort, but it is suboptimal. + // The following will remove the entry for this dictionary with the dictionary + // provider. When the metadata is downloaded again, we will try downloading it + // again. + // However, in the practice that will mean the user will find themselves without + // the new dictionary. That's fine for languages where it's included in the APK, + // but for other languages it will leave the user without a dictionary at all until + // the next update, which may be a few days away. + // Ideally, we would trigger a new download right away, and use increasing retry + // delays for this particular id/version combination. + // Then again, this is expected to only ever happen in case of human mistake. If + // the wrong file is on the server, the following is still doing the right thing. + // If it's a file left over from the last version however, it's not great. + BinaryDictionaryFileDumper.reportBrokenFileToDictionaryProvider( + providerClient, + context.getString(R.string.dictionary_pack_client_id), + wordlistId); + } + } + } + + /** * Initializes a main dictionary collection from a dictionary pack, with default flags. * * This searches for a content provider providing a dictionary pack for the specified diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java deleted file mode 100644 index 3df2a2b63..000000000 --- a/java/src/com/android/inputmethod/latin/DictionaryWriter.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin; - -import android.content.Context; - -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.makedict.DictEncoder; -import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.makedict.FusionDictionary; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; -import com.android.inputmethod.latin.makedict.UnsupportedFormatException; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - -/** - * An in memory dictionary for memorizing entries and writing a binary dictionary. - */ -public class DictionaryWriter extends AbstractDictionaryWriter { - private static final int BINARY_DICT_VERSION = 3; - private static final FormatSpec.FormatOptions FORMAT_OPTIONS = - new FormatSpec.FormatOptions(BINARY_DICT_VERSION, true /* supportsDynamicUpdate */); - - private FusionDictionary mFusionDictionary; - - public DictionaryWriter(final Context context, final String dictType) { - super(context, dictType); - clear(); - } - - @Override - public void clear() { - final HashMap<String, String> attributes = CollectionUtils.newHashMap(); - mFusionDictionary = new FusionDictionary(new PtNodeArray(), - new FusionDictionary.DictionaryOptions(attributes, false, false)); - } - - /** - * Adds a word unigram to the fusion dictionary. - */ - // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries, - // considering performance regression. - @Override - public void addUnigramWord(final String word, final String shortcutTarget, final int frequency, - final int shortcutFreq, final boolean isNotAWord) { - if (shortcutTarget == null) { - mFusionDictionary.add(word, frequency, null, isNotAWord); - } else { - // TODO: Do this in the subclass, with this class taking an arraylist. - final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList(); - shortcutTargets.add(new WeightedString(shortcutTarget, shortcutFreq)); - mFusionDictionary.add(word, frequency, shortcutTargets, isNotAWord); - } - } - - @Override - public void addBigramWords(final String word0, final String word1, final int frequency, - final boolean isValid, final long lastModifiedTime) { - mFusionDictionary.setBigram(word0, word1, frequency); - } - - @Override - public void removeBigramWords(final String word0, final String word1) { - // This class don't support removing bigram words. - } - - @Override - protected void writeDictionary(final DictEncoder dictEncoder, - final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException { - for (final Map.Entry<String, String> entry : attributeMap.entrySet()) { - mFusionDictionary.addOptionAttribute(entry.getKey(), entry.getValue()); - } - dictEncoder.writeDictionary(mFusionDictionary, FORMAT_OPTIONS); - } - - @Override - public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - // This class doesn't support suggestion. - return null; - } - - @Override - public boolean isValidWord(String word) { - // This class doesn't support dictionary retrieval. - return false; - } -} diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java index eb8650e6f..64e9d2b51 100644 --- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java @@ -17,23 +17,31 @@ package com.android.inputmethod.latin; import android.content.Context; -import android.os.SystemClock; import android.util.Log; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.personalization.DynamicPersonalizationDictionaryWriter; +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.utils.AsyncResultHolder; +import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.PrioritizedSerialExecutor; +import com.android.inputmethod.latin.utils.CombinedFormatUtils; +import com.android.inputmethod.latin.utils.ExecutorUtils; +import com.android.inputmethod.latin.utils.FileUtils; +import com.android.inputmethod.latin.utils.LanguageModelParam; import java.io.File; import java.util.ArrayList; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -52,34 +60,29 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { /** Whether to print debug output to log */ private static boolean DEBUG = false; - - // TODO: Remove. - /** Whether to call binary dictionary dynamically updating methods. */ - public static boolean ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE = true; + private static final boolean DBG_STRESS_TEST = false; private static final int TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS = 100; + private static final int TIMEOUT_FOR_READ_OPS_FOR_TESTS_IN_MILLISECONDS = 10000; + + private static final int DEFAULT_MAX_UNIGRAM_COUNT = 10000; + private static final int DEFAULT_MAX_BIGRAM_COUNT = 10000; /** * The maximum length of a word in this dictionary. */ protected static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH; - private static final int DICTIONARY_FORMAT_VERSION = 3; - - private static final String SUPPORTS_DYNAMIC_UPDATE = - FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE; + private static final int DICTIONARY_FORMAT_VERSION = FormatSpec.VERSION4; /** * A static map of update controllers, each of which records the time of accesses to a single * binary dictionary file and tracks whether the file is regenerating. The key for this map is - * the filename and the value is the shared dictionary time recorder associated with that - * filename. + * the dictionary name and the value is the shared dictionary time recorder associated with + * that dictionary name. */ private static final ConcurrentHashMap<String, DictionaryUpdateController> - sFilenameDictionaryUpdateControllerMap = CollectionUtils.newConcurrentHashMap(); - - private static final ConcurrentHashMap<String, PrioritizedSerialExecutor> - sFilenameExecutorMap = CollectionUtils.newConcurrentHashMap(); + sDictNameDictionaryUpdateControllerMap = CollectionUtils.newConcurrentHashMap(); /** The application context. */ protected final Context mContext; @@ -90,23 +93,22 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ private BinaryDictionary mBinaryDictionary; - // TODO: Remove and handle dictionaries in native code. - /** The in-memory dictionary used to generate the binary dictionary. */ - protected AbstractDictionaryWriter mDictionaryWriter; - /** - * The name of this dictionary, used as the filename for storing the binary dictionary. Multiple - * dictionary instances with the same filename is supported, with access controlled by - * DictionaryTimeRecorder. + * The name of this dictionary, used as a part of the filename for storing the binary + * dictionary. Multiple dictionary instances with the same name is supported, with access + * controlled by DictionaryUpdateController. */ - private final String mFilename; + private final String mDictName; - /** Whether to support dynamically updating the dictionary */ - private final boolean mIsUpdatable; + /** Dictionary locale */ + private final Locale mLocale; + + /** Dictionary file */ + private final File mDictFile; // TODO: remove, once dynamic operations is serialized /** Controls updating the shared binary dictionary file across multiple instances. */ - private final DictionaryUpdateController mFilenameDictionaryUpdateController; + private final DictionaryUpdateController mDictNameDictionaryUpdateController; // TODO: remove, once dynamic operations is serialized /** Controls updating the local binary dictionary for this instance. */ @@ -114,90 +116,82 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { new DictionaryUpdateController(); /* A extension for a binary dictionary file. */ - public static final String DICT_FILE_EXTENSION = ".dict"; + protected static final String DICT_FILE_EXTENSION = ".dict"; private final AtomicReference<Runnable> mUnfinishedFlushingTask = new AtomicReference<Runnable>(); /** - * Abstract method for loading the unigrams and bigrams of a given dictionary in a background - * thread. + * Abstract method for loading initial contents of a given dictionary. */ - protected abstract void loadDictionaryAsync(); + protected abstract void loadInitialContentsLocked(); /** - * Indicates that the source dictionary content has changed and a rebuild of the binary file is - * required. If it returns false, the next reload will only read the current binary dictionary - * from file. Note that the shared binary dictionary is locked when this is called. + * Indicates that the source dictionary contents have changed and a rebuild of the binary file + * is required. If it returns false, the next reload will only read the current binary + * dictionary from file. Note that the shared binary dictionary is locked when this is called. */ - protected abstract boolean hasContentChanged(); + protected abstract boolean haveContentsChanged(); + + private boolean matchesExpectedBinaryDictFormatVersionForThisType(final int formatVersion) { + return formatVersion == FormatSpec.VERSION4; + } + + private boolean needsToMigrateDictionary(final int formatVersion) { + // TODO: Check version. + return false; + } + + public boolean isValidDictionaryLocked() { + return mBinaryDictionary.isValidDictionary(); + } /** - * Gets the dictionary update controller for the given filename. + * Gets the dictionary update controller for the given dictionary name. */ private static DictionaryUpdateController getDictionaryUpdateController( - String filename) { - DictionaryUpdateController recorder = sFilenameDictionaryUpdateControllerMap.get(filename); + final String dictName) { + DictionaryUpdateController recorder = sDictNameDictionaryUpdateControllerMap.get(dictName); if (recorder == null) { - synchronized(sFilenameDictionaryUpdateControllerMap) { + synchronized(sDictNameDictionaryUpdateControllerMap) { recorder = new DictionaryUpdateController(); - sFilenameDictionaryUpdateControllerMap.put(filename, recorder); + sDictNameDictionaryUpdateControllerMap.put(dictName, recorder); } } return recorder; } /** - * Gets the executor for the given filename. - */ - private static PrioritizedSerialExecutor getExecutor(final String filename) { - PrioritizedSerialExecutor executor = sFilenameExecutorMap.get(filename); - if (executor == null) { - synchronized(sFilenameExecutorMap) { - executor = new PrioritizedSerialExecutor(); - sFilenameExecutorMap.put(filename, executor); - } - } - return executor; - } - - private static AbstractDictionaryWriter getDictionaryWriter(final Context context, - final String dictType, final boolean isDynamicPersonalizationDictionary) { - if (isDynamicPersonalizationDictionary) { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - return null; - } else { - return new DynamicPersonalizationDictionaryWriter(context, dictType); - } - } else { - return new DictionaryWriter(context, dictType); - } - } - - /** * Creates a new expandable binary dictionary. * * @param context The application context of the parent. - * @param filename The filename for this binary dictionary. Multiple dictionaries with the same - * filename is supported. + * @param dictName The name of the dictionary. Multiple instances with the same + * name is supported. + * @param locale the dictionary locale. * @param dictType the dictionary type, as a human-readable string - * @param isUpdatable whether to support dynamically updating the dictionary. Please note that - * dynamic dictionary has negative effects on memory space and computation time. + * @param dictFile dictionary file path. if null, use default dictionary path based on + * dictionary type. */ - public ExpandableBinaryDictionary(final Context context, final String filename, - final String dictType, final boolean isUpdatable) { + public ExpandableBinaryDictionary(final Context context, final String dictName, + final Locale locale, final String dictType, final File dictFile) { super(dictType); - mFilename = filename; + mDictName = dictName; mContext = context; - mIsUpdatable = isUpdatable; + mLocale = locale; + mDictFile = getDictFile(context, dictName, dictFile); mBinaryDictionary = null; - mFilenameDictionaryUpdateController = getDictionaryUpdateController(filename); - // Currently, only dynamic personalization dictionary is updatable. - mDictionaryWriter = getDictionaryWriter(context, dictType, isUpdatable); + mDictNameDictionaryUpdateController = getDictionaryUpdateController(dictName); } - protected static String getFilenameWithLocale(final String name, final String localeStr) { - return name + "." + localeStr + DICT_FILE_EXTENSION; + public static File getDictFile(final Context context, final String dictName, + final File dictFile) { + return (dictFile != null) ? dictFile + : new File(context.getFilesDir(), dictName + DICT_FILE_EXTENSION); + } + + public static String getDictName(final String name, final Locale locale, + final File dictFile) { + return dictFile != null ? dictFile.getName() : name + "." + locale.toString(); } /** @@ -205,23 +199,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ @Override public void close() { - getExecutor(mFilename).execute(new Runnable() { - @Override - public void run() { - if (mBinaryDictionary!= null) { - mBinaryDictionary.close(); - mBinaryDictionary = null; - } - if (mDictionaryWriter != null) { - mDictionaryWriter.close(); - } - } - }); - } - - protected void closeBinaryDictionary() { - // Ensure that no other threads are accessing the local binary dictionary. - getExecutor(mFilename).execute(new Runnable() { + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { @Override public void run() { if (mBinaryDictionary != null) { @@ -234,80 +212,82 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { protected Map<String, String> getHeaderAttributeMap() { HashMap<String, String> attributeMap = new HashMap<String, String>(); - attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE, - SUPPORTS_DYNAMIC_UPDATE); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFilename); + attributeMap.put(DictionaryHeader.DICTIONARY_ID_KEY, mDictName); + attributeMap.put(DictionaryHeader.DICTIONARY_LOCALE_KEY, mLocale.toString()); + attributeMap.put(DictionaryHeader.DICTIONARY_VERSION_KEY, + String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))); + attributeMap.put(DictionaryHeader.MAX_UNIGRAM_COUNT_KEY, + String.valueOf(DEFAULT_MAX_UNIGRAM_COUNT)); + attributeMap.put(DictionaryHeader.MAX_BIGRAM_COUNT_KEY, + String.valueOf(DEFAULT_MAX_BIGRAM_COUNT)); return attributeMap; } + private void removeBinaryDictionaryLocked() { + if (mBinaryDictionary != null) { + mBinaryDictionary.close(); + } + if (mDictFile.exists() && !FileUtils.deleteRecursively(mDictFile)) { + Log.e(TAG, "Can't remove a file: " + mDictFile.getName()); + } + mBinaryDictionary = null; + } + + private void createBinaryDictionaryLocked() { + BinaryDictionaryUtils.createEmptyDictFile(mDictFile.getAbsolutePath(), + DICTIONARY_FORMAT_VERSION, mLocale, getHeaderAttributeMap()); + } + + private void openBinaryDictionaryLocked() { + mBinaryDictionary = new BinaryDictionary( + mDictFile.getAbsolutePath(), 0 /* offset */, mDictFile.length(), + true /* useFullEditDistance */, mLocale, mDictType, true /* isUpdatable */); + } + protected void clear() { - getExecutor(mFilename).execute(new Runnable() { + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { @Override public void run() { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE && mDictionaryWriter == null) { - mBinaryDictionary.close(); - final File file = new File(mContext.getFilesDir(), mFilename); - BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), - DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); - mBinaryDictionary = new BinaryDictionary( - file.getAbsolutePath(), 0 /* offset */, file.length(), - true /* useFullEditDistance */, null, mDictType, mIsUpdatable); - } else { - mDictionaryWriter.clear(); - } + removeBinaryDictionaryLocked(); + createBinaryDictionaryLocked(); + openBinaryDictionaryLocked(); } }); } /** - * Adds a word unigram to the dictionary. Used for loading a dictionary. - * @param word The word to add. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this unigram. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored - * if shortcutTarget is null. - * @param isNotAWord true if this is not a word, i.e. shortcut only. - */ - protected void addWord(final String word, final String shortcutTarget, - final int frequency, final int shortcutFreq, final boolean isNotAWord) { - mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, shortcutFreq, isNotAWord); - } - - /** - * Adds a word bigram in the dictionary. Used for loading a dictionary. - */ - protected void addBigram(final String prevWord, final String word, final int frequency, - final long lastModifiedTime) { - mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */, - lastModifiedTime); - } - - /** * Check whether GC is needed and run GC if required. */ protected void runGCIfRequired(final boolean mindsBlockByGC) { - if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) return; - getExecutor(mFilename).execute(new Runnable() { + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { @Override public void run() { - runGCIfRequiredInternalLocked(mindsBlockByGC); + if (mBinaryDictionary == null) { + return; + } + runGCAfterAllPrioritizedTasksIfRequiredLocked(mindsBlockByGC); } }); } - private void runGCIfRequiredInternalLocked(final boolean mindsBlockByGC) { - if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) return; - // Calls to needsToRunGC() need to be serialized. + protected void runGCIfRequiredLocked(final boolean mindsBlockByGC) { + if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) { + mBinaryDictionary.flushWithGC(); + } + } + + private void runGCAfterAllPrioritizedTasksIfRequiredLocked(final boolean mindsBlockByGC) { + // needsToRunGC() have to be called with lock. if (mBinaryDictionary.needsToRunGC(mindsBlockByGC)) { - if (setIsRegeneratingIfNotRegenerating()) { + if (setProcessingLargeTaskIfNot()) { // Run GC after currently existing time sensitive operations. - getExecutor(mFilename).executePrioritized(new Runnable() { + ExecutorUtils.getExecutor(mDictName).executePrioritized(new Runnable() { @Override public void run() { try { mBinaryDictionary.flushWithGC(); } finally { - mFilenameDictionaryUpdateController.mIsRegenerating.set(false); + mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false); } } }); @@ -318,70 +298,99 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { /** * Dynamically adds a word unigram to the dictionary. May overwrite an existing entry. */ - protected void addWordDynamically(final String word, final String shortcutTarget, - final int frequency, final int shortcutFreq, final boolean isNotAWord) { - if (!mIsUpdatable) { - Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename); - return; - } - getExecutor(mFilename).execute(new Runnable() { + protected void addWordDynamically(final String word, final int frequency, + final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, + final boolean isBlacklisted, final int timestamp) { + reloadDictionaryIfRequired(); + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { @Override public void run() { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - runGCIfRequiredInternalLocked(true /* mindsBlockByGC */); - mBinaryDictionary.addUnigramWord(word, frequency); - } else { - // TODO: Remove. - mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, shortcutFreq, - isNotAWord); + if (mBinaryDictionary == null) { + return; } + runGCAfterAllPrioritizedTasksIfRequiredLocked(true /* mindsBlockByGC */); + addWordDynamicallyLocked(word, frequency, shortcutTarget, shortcutFreq, + isNotAWord, isBlacklisted, timestamp); } }); } + protected void addWordDynamicallyLocked(final String word, final int frequency, + final String shortcutTarget, final int shortcutFreq, final boolean isNotAWord, + final boolean isBlacklisted, final int timestamp) { + mBinaryDictionary.addUnigramWord(word, frequency, shortcutTarget, shortcutFreq, + isNotAWord, isBlacklisted, timestamp); + } + /** * Dynamically adds a word bigram in the dictionary. May overwrite an existing entry. */ protected void addBigramDynamically(final String word0, final String word1, - final int frequency, final boolean isValid) { - if (!mIsUpdatable) { - Log.w(TAG, "addBigramDynamically is called for non-updatable dictionary: " - + mFilename); - return; - } - getExecutor(mFilename).execute(new Runnable() { + final int frequency, final int timestamp) { + reloadDictionaryIfRequired(); + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { @Override public void run() { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - runGCIfRequiredInternalLocked(true /* mindsBlockByGC */); - mBinaryDictionary.addBigramWords(word0, word1, frequency); - } else { - // TODO: Remove. - mDictionaryWriter.addBigramWords(word0, word1, frequency, isValid, - 0 /* lastTouchedTime */); + if (mBinaryDictionary == null) { + return; } + runGCAfterAllPrioritizedTasksIfRequiredLocked(true /* mindsBlockByGC */); + addBigramDynamicallyLocked(word0, word1, frequency, timestamp); } }); } + protected void addBigramDynamicallyLocked(final String word0, final String word1, + final int frequency, final int timestamp) { + mBinaryDictionary.addBigramWords(word0, word1, frequency, timestamp); + } + /** * Dynamically remove a word bigram in the dictionary. */ protected void removeBigramDynamically(final String word0, final String word1) { - if (!mIsUpdatable) { - Log.w(TAG, "removeBigramDynamically is called for non-updatable dictionary: " - + mFilename); - return; - } - getExecutor(mFilename).execute(new Runnable() { + reloadDictionaryIfRequired(); + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { @Override public void run() { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - runGCIfRequiredInternalLocked(true /* mindsBlockByGC */); - mBinaryDictionary.removeBigramWords(word0, word1); - } else { - // TODO: Remove. - mDictionaryWriter.removeBigramWords(word0, word1); + if (mBinaryDictionary == null) { + return; + } + runGCAfterAllPrioritizedTasksIfRequiredLocked(true /* mindsBlockByGC */); + mBinaryDictionary.removeBigramWords(word0, word1); + } + }); + } + + public interface AddMultipleDictionaryEntriesCallback { + public void onFinished(); + } + + /** + * Dynamically add multiple entries to the dictionary. + */ + protected void addMultipleDictionaryEntriesDynamically( + final ArrayList<LanguageModelParam> languageModelParams, + final AddMultipleDictionaryEntriesCallback callback) { + reloadDictionaryIfRequired(); + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { + @Override + public void run() { + if (mBinaryDictionary == null) { + return; + } + final boolean locked = setProcessingLargeTaskIfNot(); + try { + mBinaryDictionary.addMultipleDictionaryEntries( + languageModelParams.toArray( + new LanguageModelParam[languageModelParams.size()])); + } finally { + if (callback != null) { + callback.onFinished(); + } + if (locked) { + mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false); + } } } }); @@ -391,50 +400,27 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, - final int sessionId) { + final int sessionId, final float[] inOutLanguageWeight) { reloadDictionaryIfRequired(); - if (isRegenerating()) { + if (processingLargeTask()) { return null; } - final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); final AsyncResultHolder<ArrayList<SuggestedWordInfo>> holder = new AsyncResultHolder<ArrayList<SuggestedWordInfo>>(); - getExecutor(mFilename).executePrioritized(new Runnable() { + ExecutorUtils.getExecutor(mDictName).executePrioritized(new Runnable() { @Override public void run() { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - if (mBinaryDictionary == null) { - holder.set(null); - return; - } - final ArrayList<SuggestedWordInfo> binarySuggestion = - mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord, - proximityInfo, blockOffensiveWords, additionalFeaturesOptions, - sessionId); - holder.set(binarySuggestion); - } else { - final ArrayList<SuggestedWordInfo> inMemDictSuggestion = - composer.isBatchMode() ? null : - mDictionaryWriter.getSuggestionsWithSessionId(composer, - prevWord, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, sessionId); - // TODO: Remove checking mIsUpdatable and use native suggestion. - if (mBinaryDictionary != null && !mIsUpdatable) { - final ArrayList<SuggestedWordInfo> binarySuggestion = - mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord, - proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, sessionId); - if (inMemDictSuggestion == null) { - holder.set(binarySuggestion); - } else if (binarySuggestion == null) { - holder.set(inMemDictSuggestion); - } else { - binarySuggestion.addAll(inMemDictSuggestion); - holder.set(binarySuggestion); - } - } else { - holder.set(inMemDictSuggestion); - } + if (mBinaryDictionary == null) { + holder.set(null); + return; + } + final ArrayList<SuggestedWordInfo> binarySuggestion = + mBinaryDictionary.getSuggestionsWithSessionId(composer, prevWord, + proximityInfo, blockOffensiveWords, additionalFeaturesOptions, + sessionId, inOutLanguageWeight); + holder.set(binarySuggestion); + if (mBinaryDictionary.isCorrupted()) { + removeBinaryDictionaryLocked(); } } }); @@ -444,23 +430,20 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final float[] inOutLanguageWeight) { return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, 0 /* sessionId */); + additionalFeaturesOptions, 0 /* sessionId */, inOutLanguageWeight); } @Override public boolean isValidWord(final String word) { reloadDictionaryIfRequired(); - return isValidWordInner(word); - } - - protected boolean isValidWordInner(final String word) { - if (isRegenerating()) { + if (processingLargeTask()) { return false; } final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>(); - getExecutor(mFilename).executePrioritized(new Runnable() { + ExecutorUtils.getExecutor(mDictName).executePrioritized(new Runnable() { @Override public void run() { holder.set(isValidWordLocked(word)); @@ -484,7 +467,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * dictionary exists, this method will generate one. */ protected void loadDictionary() { - mPerInstanceDictionaryUpdateController.mLastUpdateRequestTime = SystemClock.uptimeMillis(); + mPerInstanceDictionaryUpdateController.mLastUpdateRequestTime = System.currentTimeMillis(); reloadDictionaryIfRequired(); } @@ -492,71 +475,57 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * Loads the current binary dictionary from internal storage. Assumes the dictionary file * exists. */ - private void loadBinaryDictionary() { + private void loadBinaryDictionaryLocked() { if (DEBUG) { - Log.d(TAG, "Loading binary dictionary: " + mFilename + " request=" - + mFilenameDictionaryUpdateController.mLastUpdateRequestTime + " update=" - + mFilenameDictionaryUpdateController.mLastUpdateTime); + Log.d(TAG, "Loading binary dictionary: " + mDictName + " request=" + + mDictNameDictionaryUpdateController.mLastUpdateRequestTime + " update=" + + mDictNameDictionaryUpdateController.mLastUpdateTime); } - - final File file = new File(mContext.getFilesDir(), mFilename); - final String filename = file.getAbsolutePath(); - final long length = file.length(); - - // Build the new binary dictionary - final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0 /* offset */, - length, true /* useFullEditDistance */, null, mDictType, mIsUpdatable); - - // Ensure all threads accessing the current dictionary have finished before - // swapping in the new one. - // TODO: Ensure multi-thread assignment of mBinaryDictionary. - final BinaryDictionary oldBinaryDictionary = mBinaryDictionary; - getExecutor(mFilename).executePrioritized(new Runnable() { - @Override - public void run() { - mBinaryDictionary = newBinaryDictionary; - if (oldBinaryDictionary != null) { - oldBinaryDictionary.close(); - } + if (DBG_STRESS_TEST) { + // Test if this class does not cause problems when it takes long time to load binary + // dictionary. + try { + Log.w(TAG, "Start stress in loading: " + mDictName); + Thread.sleep(15000); + Log.w(TAG, "End stress in loading"); + } catch (InterruptedException e) { } - }); + } + final BinaryDictionary oldBinaryDictionary = mBinaryDictionary; + openBinaryDictionaryLocked(); + if (oldBinaryDictionary != null) { + oldBinaryDictionary.close(); + } + if (mBinaryDictionary.isValidDictionary() + && needsToMigrateDictionary(mBinaryDictionary.getFormatVersion())) { + mBinaryDictionary.migrateTo(DICTIONARY_FORMAT_VERSION); + } } /** - * Abstract method for checking if it is required to reload the dictionary before writing - * a binary dictionary. + * Create a new binary dictionary and load initial contents. */ - abstract protected boolean needsToReloadBeforeWriting(); - - /** - * Writes a new binary dictionary based on the contents of the fusion dictionary. - */ - private void writeBinaryDictionary() { + private void createNewDictionaryLocked() { if (DEBUG) { - Log.d(TAG, "Generating binary dictionary: " + mFilename + " request=" - + mFilenameDictionaryUpdateController.mLastUpdateRequestTime + " update=" - + mFilenameDictionaryUpdateController.mLastUpdateTime); + Log.d(TAG, "Generating binary dictionary: " + mDictName + " request=" + + mDictNameDictionaryUpdateController.mLastUpdateRequestTime + " update=" + + mDictNameDictionaryUpdateController.mLastUpdateTime); } - if (needsToReloadBeforeWriting()) { - mDictionaryWriter.clear(); - loadDictionaryAsync(); - mDictionaryWriter.write(mFilename, getHeaderAttributeMap()); + removeBinaryDictionaryLocked(); + createBinaryDictionaryLocked(); + openBinaryDictionaryLocked(); + loadInitialContentsLocked(); + mBinaryDictionary.flushWithGC(); + } + + private void flushDictionaryLocked() { + if (mBinaryDictionary == null) { + return; + } + if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) { + mBinaryDictionary.flushWithGC(); } else { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - if (mBinaryDictionary == null || !mBinaryDictionary.isValidDictionary()) { - final File file = new File(mContext.getFilesDir(), mFilename); - BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), - DICTIONARY_FORMAT_VERSION, getHeaderAttributeMap()); - } else { - if (mBinaryDictionary.needsToRunGC(false /* mindsBlockByGC */)) { - mBinaryDictionary.flushWithGC(); - } else { - mBinaryDictionary.flush(); - } - } - } else { - mDictionaryWriter.write(mFilename, getHeaderAttributeMap()); - } + mBinaryDictionary.flush(); } } @@ -568,12 +537,12 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { * the current binary dictionary from file. */ protected void setRequiresReload(final boolean requiresRebuild) { - final long time = SystemClock.uptimeMillis(); + final long time = System.currentTimeMillis(); mPerInstanceDictionaryUpdateController.mLastUpdateRequestTime = time; - mFilenameDictionaryUpdateController.mLastUpdateRequestTime = time; + mDictNameDictionaryUpdateController.mLastUpdateRequestTime = time; if (DEBUG) { - Log.d(TAG, "Reload request: " + mFilename + ": request=" + time + " update=" - + mFilenameDictionaryUpdateController.mLastUpdateTime); + Log.d(TAG, "Reload request: " + mDictName + ": request=" + time + " update=" + + mDictNameDictionaryUpdateController.mLastUpdateTime); } } @@ -582,7 +551,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { */ public final void reloadDictionaryIfRequired() { if (!isReloadRequired()) return; - if (setIsRegeneratingIfNotRegenerating()) { + if (setProcessingLargeTaskIfNot()) { reloadDictionary(); } } @@ -594,13 +563,14 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { return mBinaryDictionary == null || mPerInstanceDictionaryUpdateController.isOutOfDate(); } - private boolean isRegenerating() { - return mFilenameDictionaryUpdateController.mIsRegenerating.get(); + private boolean processingLargeTask() { + return mDictNameDictionaryUpdateController.mProcessingLargeTask.get(); } - // Returns whether the dictionary can be regenerated. - private boolean setIsRegeneratingIfNotRegenerating() { - return mFilenameDictionaryUpdateController.mIsRegenerating.compareAndSet( + // Returns whether the dictionary is being used for a large task. If true, we should not use + // this dictionary for latency sensitive operations. + private boolean setProcessingLargeTaskIfNot() { + return mDictNameDictionaryUpdateController.mProcessingLargeTask.compareAndSet( false /* expect */ , true /* update */); } @@ -611,46 +581,45 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { private final void reloadDictionary() { // Ensure that only one thread attempts to read or write to the shared binary dictionary // file at the same time. - getExecutor(mFilename).execute(new Runnable() { + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { @Override public void run() { try { - final long time = SystemClock.uptimeMillis(); - final boolean dictionaryFileExists = dictionaryFileExists(); - if (mFilenameDictionaryUpdateController.isOutOfDate() - || !dictionaryFileExists) { - // If the shared dictionary file does not exist or is out of date, the - // first instance that acquires the lock will generate a new one. - if (hasContentChanged() || !dictionaryFileExists) { - // If the source content has changed or the dictionary does not exist, - // rebuild the binary dictionary. Empty dictionaries are supported (in - // the case where loadDictionaryAsync() adds nothing) in order to - // provide a uniform framework. - mFilenameDictionaryUpdateController.mLastUpdateTime = time; - writeBinaryDictionary(); - loadBinaryDictionary(); - } else { - // If not, the reload request was unnecessary so revert - // LastUpdateRequestTime to LastUpdateTime. - mFilenameDictionaryUpdateController.mLastUpdateRequestTime = - mFilenameDictionaryUpdateController.mLastUpdateTime; - } + final long time = System.currentTimeMillis(); + final boolean openedDictIsOutOfDate = + mDictNameDictionaryUpdateController.isOutOfDate(); + if (!dictionaryFileExists() + || (openedDictIsOutOfDate && haveContentsChanged())) { + // If the shared dictionary file does not exist or is out of date and + // contents have been updated, the first instance that acquires the lock + // will generate a new one + mDictNameDictionaryUpdateController.mLastUpdateTime = time; + createNewDictionaryLocked(); + } else if (openedDictIsOutOfDate) { + // If not, the reload request was unnecessary so revert + // LastUpdateRequestTime to LastUpdateTime. + mDictNameDictionaryUpdateController.mLastUpdateRequestTime = + mDictNameDictionaryUpdateController.mLastUpdateTime; } else if (mBinaryDictionary == null || mPerInstanceDictionaryUpdateController.mLastUpdateTime - < mFilenameDictionaryUpdateController.mLastUpdateTime) { + < mDictNameDictionaryUpdateController.mLastUpdateTime) { // Otherwise, if the local dictionary is older than the shared dictionary, // load the shared dictionary. - loadBinaryDictionary(); + loadBinaryDictionaryLocked(); } - if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) { - // Binary dictionary is not valid. Regenerate the dictionary file. - mFilenameDictionaryUpdateController.mLastUpdateTime = time; - writeBinaryDictionary(); - loadBinaryDictionary(); + if (mBinaryDictionary != null && !(isValidDictionaryLocked() + // TODO: remove the check below + && matchesExpectedBinaryDictFormatVersionForThisType( + mBinaryDictionary.getFormatVersion()))) { + // Binary dictionary or its format version is not valid. Regenerate + // the dictionary file. writeBinaryDictionary will remove the + // existing files if appropriate. + mDictNameDictionaryUpdateController.mLastUpdateTime = time; + createNewDictionaryLocked(); } mPerInstanceDictionaryUpdateController.mLastUpdateTime = time; } finally { - mFilenameDictionaryUpdateController.mIsRegenerating.set(false); + mDictNameDictionaryUpdateController.mProcessingLargeTask.set(false); } } }); @@ -658,79 +627,95 @@ abstract public class ExpandableBinaryDictionary extends Dictionary { // TODO: cache the file's existence so that we avoid doing a disk access each time. private boolean dictionaryFileExists() { - final File file = new File(mContext.getFilesDir(), mFilename); - return file.exists(); + return mDictFile.exists(); } /** - * Load the dictionary to memory. + * Flush binary dictionary to dictionary file. */ - protected void asyncLoadDictionaryToMemory() { - getExecutor(mFilename).executePrioritized(new Runnable() { - @Override - public void run() { - if (!ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - loadDictionaryAsync(); - } - } - }); - } - - /** - * Generate binary dictionary using DictionaryWriter. - */ - protected void asyncFlashAllBinaryDictionary() { + protected void asyncFlushBinaryDictionary() { final Runnable newTask = new Runnable() { @Override public void run() { - writeBinaryDictionary(); + flushDictionaryLocked(); } }; final Runnable oldTask = mUnfinishedFlushingTask.getAndSet(newTask); - getExecutor(mFilename).replaceAndExecute(oldTask, newTask); + ExecutorUtils.getExecutor(mDictName).replaceAndExecute(oldTask, newTask); } /** - * For tracking whether the dictionary is out of date and the dictionary is regenerating. - * Can be shared across multiple dictionary instances that access the same filename. + * For tracking whether the dictionary is out of date and the dictionary is used in a large + * task. Can be shared across multiple dictionary instances that access the same filename. */ private static class DictionaryUpdateController { public volatile long mLastUpdateTime = 0; public volatile long mLastUpdateRequestTime = 0; - public volatile AtomicBoolean mIsRegenerating = new AtomicBoolean(); + public volatile AtomicBoolean mProcessingLargeTask = new AtomicBoolean(); public boolean isOutOfDate() { return (mLastUpdateRequestTime > mLastUpdateTime); } } - // TODO: Implement native binary methods once the dynamic dictionary implementation is done. + // TODO: Implement BinaryDictionary.isInDictionary(). @UsedForTesting - public boolean isInDictionaryForTests(final String word) { + public boolean isInUnderlyingBinaryDictionaryForTests(final String word) { final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>(); - getExecutor(mFilename).executePrioritized(new Runnable() { + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { @Override public void run() { if (mDictType == Dictionary.TYPE_USER_HISTORY) { - if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - holder.set(mBinaryDictionary.isValidWord(word)); - } else { - holder.set(((DynamicPersonalizationDictionaryWriter) mDictionaryWriter) - .isInBigramListForTests(word)); - } + holder.set(mBinaryDictionary.isValidWord(word)); } } }); - return holder.get(false, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS); + return holder.get(false, TIMEOUT_FOR_READ_OPS_FOR_TESTS_IN_MILLISECONDS); } @UsedForTesting - public void shutdownExecutorForTests() { - getExecutor(mFilename).shutdown(); + public void waitAllTasksForTests() { + final CountDownLatch countDownLatch = new CountDownLatch(1); + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { + @Override + public void run() { + countDownLatch.countDown(); + } + }); + try { + countDownLatch.await(); + } catch (InterruptedException e) { + Log.e(TAG, "Interrupted while waiting for finishing dictionary operations.", e); + } } @UsedForTesting - public boolean isTerminatedForTests() { - return getExecutor(mFilename).isTerminated(); + public void dumpAllWordsForDebug() { + reloadDictionaryIfRequired(); + ExecutorUtils.getExecutor(mDictName).execute(new Runnable() { + @Override + public void run() { + Log.d(TAG, "Dump dictionary: " + mDictName); + try { + final DictionaryHeader header = mBinaryDictionary.getHeader(); + Log.d(TAG, CombinedFormatUtils.formatAttributeMap( + header.mDictionaryOptions.mAttributes)); + } catch (final UnsupportedFormatException e) { + Log.d(TAG, "Cannot fetch header information.", e); + } + int token = 0; + do { + final BinaryDictionary.GetNextWordPropertyResult result = + mBinaryDictionary.getNextWordProperty(token); + final WordProperty wordProperty = result.mWordProperty; + if (wordProperty == null) { + Log.d(TAG, " dictionary is empty."); + break; + } + Log.d(TAG, wordProperty.toString()); + token = result.mNextToken; + } while (token != 0); + } + }); } } diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java deleted file mode 100644 index 95c9bcab9..000000000 --- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java +++ /dev/null @@ -1,894 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin; - -import android.text.TextUtils; -import android.util.Log; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils.ForgettingCurveParams; - -import java.util.ArrayList; -import java.util.LinkedList; - -/** - * Class for an in-memory dictionary that can grow dynamically and can - * be searched for suggestions and valid words. - */ -// TODO: Remove after binary dictionary supports dynamic update. -public class ExpandableDictionary extends Dictionary { - private static final String TAG = ExpandableDictionary.class.getSimpleName(); - /** - * The weight to give to a word if it's length is the same as the number of typed characters. - */ - private static final int FULL_WORD_SCORE_MULTIPLIER = 2; - - private char[] mWordBuilder = new char[Constants.DICTIONARY_MAX_WORD_LENGTH]; - private int mMaxDepth; - private int mInputLength; - - private static final class Node { - char mCode; - int mFrequency; - boolean mTerminal; - Node mParent; - NodeArray mChildren; - ArrayList<char[]> mShortcutTargets; - boolean mShortcutOnly; - LinkedList<NextWord> mNGrams; // Supports ngram - } - - private static final class NodeArray { - Node[] mData; - int mLength = 0; - private static final int INCREMENT = 2; - - NodeArray() { - mData = new Node[INCREMENT]; - } - - void add(final Node n) { - if (mLength + 1 > mData.length) { - Node[] tempData = new Node[mLength + INCREMENT]; - if (mLength > 0) { - System.arraycopy(mData, 0, tempData, 0, mLength); - } - mData = tempData; - } - mData[mLength++] = n; - } - } - - public interface NextWord { - public Node getWordNode(); - public int getFrequency(); - public ForgettingCurveParams getFcParams(); - public int notifyTypedAgainAndGetFrequency(); - } - - private static final class NextStaticWord implements NextWord { - public final Node mWord; - private final int mFrequency; - public NextStaticWord(Node word, int frequency) { - mWord = word; - mFrequency = frequency; - } - - @Override - public Node getWordNode() { - return mWord; - } - - @Override - public int getFrequency() { - return mFrequency; - } - - @Override - public ForgettingCurveParams getFcParams() { - return null; - } - - @Override - public int notifyTypedAgainAndGetFrequency() { - return mFrequency; - } - } - - private static final class NextHistoryWord implements NextWord { - public final Node mWord; - public final ForgettingCurveParams mFcp; - - public NextHistoryWord(Node word, ForgettingCurveParams fcp) { - mWord = word; - mFcp = fcp; - } - - @Override - public Node getWordNode() { - return mWord; - } - - @Override - public int getFrequency() { - return mFcp.getFrequency(); - } - - @Override - public ForgettingCurveParams getFcParams() { - return mFcp; - } - - @Override - public int notifyTypedAgainAndGetFrequency() { - return mFcp.notifyTypedAgainAndGetFrequency(); - } - } - - private NodeArray mRoots; - - private int[][] mCodes; - - public ExpandableDictionary(final String dictType) { - super(dictType); - clearDictionary(); - mCodes = new int[Constants.DICTIONARY_MAX_WORD_LENGTH][]; - } - - public int getMaxWordLength() { - return Constants.DICTIONARY_MAX_WORD_LENGTH; - } - - /** - * Add a word with an optional shortcut to the dictionary. - * @param word The word to add. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this unigram. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored - * if shortcutTarget is null. - */ - public void addWord(final String word, final String shortcutTarget, final int frequency, - final int shortcutFreq) { - if (word.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH) { - return; - } - addWordRec(mRoots, word, 0, shortcutTarget, frequency, shortcutFreq, null); - } - - /** - * Add a word, recursively searching for its correct place in the trie tree. - * @param children The node to recursively search for addition. Initially, the root of the tree. - * @param word The word to add. - * @param depth The current depth in the tree. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this unigram. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored - * if shortcutTarget is null. - * @param parentNode The parent node, for up linking. Initially null, as the root has no parent. - */ - private void addWordRec(final NodeArray children, final String word, final int depth, - final String shortcutTarget, final int frequency, final int shortcutFreq, - final Node parentNode) { - final int wordLength = word.length(); - if (wordLength <= depth) return; - final char c = word.charAt(depth); - // Does children have the current character? - final int childrenLength = children.mLength; - Node childNode = null; - for (int i = 0; i < childrenLength; i++) { - final Node node = children.mData[i]; - if (node.mCode == c) { - childNode = node; - break; - } - } - final boolean isShortcutOnly = (null != shortcutTarget); - if (childNode == null) { - childNode = new Node(); - childNode.mCode = c; - childNode.mParent = parentNode; - childNode.mShortcutOnly = isShortcutOnly; - children.add(childNode); - } - if (wordLength == depth + 1) { - // Terminate this word - childNode.mTerminal = true; - if (isShortcutOnly) { - if (null == childNode.mShortcutTargets) { - childNode.mShortcutTargets = CollectionUtils.newArrayList(); - } - childNode.mShortcutTargets.add(shortcutTarget.toCharArray()); - } else { - childNode.mShortcutOnly = false; - } - childNode.mFrequency = Math.max(frequency, childNode.mFrequency); - if (childNode.mFrequency > 255) childNode.mFrequency = 255; - return; - } - if (childNode.mChildren == null) { - childNode.mChildren = new NodeArray(); - } - addWordRec(childNode.mChildren, word, depth + 1, shortcutTarget, frequency, shortcutFreq, - childNode); - } - - @Override - public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - if (composer.size() > 1) { - if (composer.size() >= Constants.DICTIONARY_MAX_WORD_LENGTH) { - return null; - } - final ArrayList<SuggestedWordInfo> suggestions = - getWordsInner(composer, prevWord, proximityInfo); - return suggestions; - } else { - if (TextUtils.isEmpty(prevWord)) return null; - final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); - runBigramReverseLookUp(prevWord, suggestions); - return suggestions; - } - } - - private ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes, - final String prevWordForBigrams, final ProximityInfo proximityInfo) { - final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); - mInputLength = codes.size(); - if (mCodes.length < mInputLength) mCodes = new int[mInputLength][]; - final InputPointers ips = codes.getInputPointers(); - final int[] xCoordinates = ips.getXCoordinates(); - final int[] yCoordinates = ips.getYCoordinates(); - // Cache the codes so that we don't have to lookup an array list - for (int i = 0; i < mInputLength; i++) { - // TODO: Calculate proximity info here. - if (mCodes[i] == null || mCodes[i].length < 1) { - mCodes[i] = new int[ProximityInfo.MAX_PROXIMITY_CHARS_SIZE]; - } - final int x = xCoordinates != null && i < xCoordinates.length ? - xCoordinates[i] : Constants.NOT_A_COORDINATE; - final int y = xCoordinates != null && i < yCoordinates.length ? - yCoordinates[i] : Constants.NOT_A_COORDINATE; - proximityInfo.fillArrayWithNearestKeyCodes(x, y, codes.getCodeAt(i), mCodes[i]); - } - mMaxDepth = mInputLength * 3; - getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, suggestions); - for (int i = 0; i < mInputLength; i++) { - getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, suggestions); - } - return suggestions; - } - - @Override - public synchronized boolean isValidWord(final String word) { - final Node node = searchNode(mRoots, word, 0, word.length()); - // If node is null, we didn't find the word, so it's not valid. - // If node.mShortcutOnly is true, then it exists as a shortcut but not as a word, - // so that means it's not a valid word. - // If node.mShortcutOnly is false, then it exists as a word (it may also exist as - // a shortcut, but this does not matter), so it's a valid word. - return (node == null) ? false : !node.mShortcutOnly; - } - - public boolean removeBigram(final String word0, final String word1) { - // Refer to addOrSetBigram() about word1.toLowerCase() - final Node firstWord = searchWord(mRoots, word0.toLowerCase(), 0, null); - final Node secondWord = searchWord(mRoots, word1, 0, null); - LinkedList<NextWord> bigrams = firstWord.mNGrams; - NextWord bigramNode = null; - if (bigrams == null || bigrams.size() == 0) { - return false; - } else { - for (NextWord nw : bigrams) { - if (nw.getWordNode() == secondWord) { - bigramNode = nw; - break; - } - } - } - if (bigramNode == null) { - return false; - } - return bigrams.remove(bigramNode); - } - - /** - * Returns the word's frequency or -1 if not found - */ - @UsedForTesting - public int getWordFrequency(final String word) { - // Case-sensitive search - final Node node = searchNode(mRoots, word, 0, word.length()); - return (node == null) ? -1 : node.mFrequency; - } - - public NextWord getBigramWord(final String word0, final String word1) { - // Refer to addOrSetBigram() about word0.toLowerCase() - final Node firstWord = searchWord(mRoots, word0.toLowerCase(), 0, null); - final Node secondWord = searchWord(mRoots, word1, 0, null); - LinkedList<NextWord> bigrams = firstWord.mNGrams; - if (bigrams == null || bigrams.size() == 0) { - return null; - } else { - for (NextWord nw : bigrams) { - if (nw.getWordNode() == secondWord) { - return nw; - } - } - } - return null; - } - - private static int computeSkippedWordFinalFreq(final int freq, final int snr, - final int inputLength) { - // The computation itself makes sense for >= 2, but the == 2 case returns 0 - // anyway so we may as well test against 3 instead and return the constant - if (inputLength >= 3) { - return (freq * snr * (inputLength - 2)) / (inputLength - 1); - } else { - return 0; - } - } - - /** - * Helper method to add a word and its shortcuts. - * - * @param node the terminal node - * @param word the word to insert, as an array of code points - * @param depth the depth of the node in the tree - * @param finalFreq the frequency for this word - * @param suggestions the suggestion collection to add the suggestions to - * @return whether there is still space for more words. - */ - private boolean addWordAndShortcutsFromNode(final Node node, final char[] word, final int depth, - final int finalFreq, final ArrayList<SuggestedWordInfo> suggestions) { - if (finalFreq > 0 && !node.mShortcutOnly) { - // Use KIND_CORRECTION always. This dictionary does not really have a notion of - // COMPLETION against CORRECTION; we could artificially add one by looking at - // the respective size of the typed word and the suggestion if it matters sometime - // in the future. - suggestions.add(new SuggestedWordInfo(new String(word, 0, depth + 1), finalFreq, - SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); - if (suggestions.size() >= Suggest.MAX_SUGGESTIONS) return false; - } - if (null != node.mShortcutTargets) { - final int length = node.mShortcutTargets.size(); - for (int shortcutIndex = 0; shortcutIndex < length; ++shortcutIndex) { - final char[] shortcut = node.mShortcutTargets.get(shortcutIndex); - suggestions.add(new SuggestedWordInfo(new String(shortcut, 0, shortcut.length), - finalFreq, SuggestedWordInfo.KIND_SHORTCUT, this /* sourceDict */, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); - if (suggestions.size() > Suggest.MAX_SUGGESTIONS) return false; - } - } - return true; - } - - /** - * Recursively traverse the tree for words that match the input. Input consists of - * a list of arrays. Each item in the list is one input character position. An input - * character is actually an array of multiple possible candidates. This function is not - * optimized for speed, assuming that the user dictionary will only be a few hundred words in - * size. - * @param roots node whose children have to be search for matches - * @param codes the input character codes - * @param word the word being composed as a possible match - * @param depth the depth of traversal - the length of the word being composed thus far - * @param completion whether the traversal is now in completion mode - meaning that we've - * exhausted the input and we're looking for all possible suffixes. - * @param snr current weight of the word being formed - * @param inputIndex position in the input characters. This can be off from the depth in - * case we skip over some punctuations such as apostrophe in the traversal. That is, if you type - * "wouldve", it could be matching "would've", so the depth will be one more than the - * inputIndex - * @param suggestions the list in which to add suggestions - */ - // TODO: Share this routine with the native code for BinaryDictionary - private void getWordsRec(final NodeArray roots, final WordComposer codes, final char[] word, - final int depth, final boolean completion, final int snr, final int inputIndex, - final int skipPos, final ArrayList<SuggestedWordInfo> suggestions) { - final int count = roots.mLength; - final int codeSize = mInputLength; - // Optimization: Prune out words that are too long compared to how much was typed. - if (depth > mMaxDepth) { - return; - } - final int[] currentChars; - if (codeSize <= inputIndex) { - currentChars = null; - } else { - currentChars = mCodes[inputIndex]; - } - - for (int i = 0; i < count; i++) { - final Node node = roots.mData[i]; - final char c = node.mCode; - final char lowerC = toLowerCase(c); - final boolean terminal = node.mTerminal; - final NodeArray children = node.mChildren; - final int freq = node.mFrequency; - if (completion || currentChars == null) { - word[depth] = c; - if (terminal) { - final int finalFreq; - if (skipPos < 0) { - finalFreq = freq * snr; - } else { - finalFreq = computeSkippedWordFinalFreq(freq, snr, mInputLength); - } - if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, suggestions)) { - // No space left in the queue, bail out - return; - } - } - if (children != null) { - getWordsRec(children, codes, word, depth + 1, true, snr, inputIndex, - skipPos, suggestions); - } - } else if ((c == Constants.CODE_SINGLE_QUOTE - && currentChars[0] != Constants.CODE_SINGLE_QUOTE) || depth == skipPos) { - // Skip the ' and continue deeper - word[depth] = c; - if (children != null) { - getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex, - skipPos, suggestions); - } - } else { - // Don't use alternatives if we're looking for missing characters - final int alternativesSize = skipPos >= 0 ? 1 : currentChars.length; - for (int j = 0; j < alternativesSize; j++) { - final int addedAttenuation = (j > 0 ? 1 : 2); - final int currentChar = currentChars[j]; - if (currentChar == Constants.NOT_A_CODE) { - break; - } - if (currentChar == lowerC || currentChar == c) { - word[depth] = c; - - if (codeSize == inputIndex + 1) { - if (terminal) { - final int finalFreq; - if (skipPos < 0) { - finalFreq = freq * snr * addedAttenuation - * FULL_WORD_SCORE_MULTIPLIER; - } else { - finalFreq = computeSkippedWordFinalFreq(freq, - snr * addedAttenuation, mInputLength); - } - if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, - suggestions)) { - // No space left in the queue, bail out - return; - } - } - if (children != null) { - getWordsRec(children, codes, word, depth + 1, - true, snr * addedAttenuation, inputIndex + 1, - skipPos, suggestions); - } - } else if (children != null) { - getWordsRec(children, codes, word, depth + 1, - false, snr * addedAttenuation, inputIndex + 1, - skipPos, suggestions); - } - } - } - } - } - } - - public int setBigramAndGetFrequency(final String word0, final String word1, - final int frequency) { - return setBigramAndGetFrequency(word0, word1, frequency, null /* unused */); - } - - public int setBigramAndGetFrequency(final String word0, final String word1, - final ForgettingCurveParams fcp) { - return setBigramAndGetFrequency(word0, word1, 0 /* unused */, fcp); - } - - /** - * Adds bigrams to the in-memory trie structure that is being used to retrieve any word - * @param word0 the first word of this bigram - * @param word1 the second word of this bigram - * @param frequency frequency for this bigram - * @param fcp an instance of ForgettingCurveParams to use for decay policy - * @return returns the final bigram frequency - */ - private int setBigramAndGetFrequency(final String word0, final String word1, - final int frequency, final ForgettingCurveParams fcp) { - if (TextUtils.isEmpty(word0)) { - Log.e(TAG, "Invalid bigram previous word: " + word0); - return frequency; - } - // We don't want results to be different according to case of the looked up left hand side - // word. We do want however to return the correct case for the right hand side. - // So we want to squash the case of the left hand side, and preserve that of the right - // hand side word. - final String word0Lower = word0.toLowerCase(); - if (TextUtils.isEmpty(word0Lower) || TextUtils.isEmpty(word1)) { - Log.e(TAG, "Invalid bigram pair: " + word0 + ", " + word0Lower + ", " + word1); - return frequency; - } - final Node firstWord = searchWord(mRoots, word0Lower, 0, null); - final Node secondWord = searchWord(mRoots, word1, 0, null); - LinkedList<NextWord> bigrams = firstWord.mNGrams; - if (bigrams == null || bigrams.size() == 0) { - firstWord.mNGrams = CollectionUtils.newLinkedList(); - bigrams = firstWord.mNGrams; - } else { - for (NextWord nw : bigrams) { - if (nw.getWordNode() == secondWord) { - return nw.notifyTypedAgainAndGetFrequency(); - } - } - } - if (fcp != null) { - // history - firstWord.mNGrams.add(new NextHistoryWord(secondWord, fcp)); - } else { - firstWord.mNGrams.add(new NextStaticWord(secondWord, frequency)); - } - return frequency; - } - - /** - * Searches for the word and add the word if it does not exist. - * @return Returns the terminal node of the word we are searching for. - */ - private Node searchWord(final NodeArray children, final String word, final int depth, - final Node parentNode) { - final int wordLength = word.length(); - final char c = word.charAt(depth); - // Does children have the current character? - final int childrenLength = children.mLength; - Node childNode = null; - for (int i = 0; i < childrenLength; i++) { - final Node node = children.mData[i]; - if (node.mCode == c) { - childNode = node; - break; - } - } - if (childNode == null) { - childNode = new Node(); - childNode.mCode = c; - childNode.mParent = parentNode; - children.add(childNode); - } - if (wordLength == depth + 1) { - // Terminate this word - childNode.mTerminal = true; - return childNode; - } - if (childNode.mChildren == null) { - childNode.mChildren = new NodeArray(); - } - return searchWord(childNode.mChildren, word, depth + 1, childNode); - } - - private void runBigramReverseLookUp(final String previousWord, - final ArrayList<SuggestedWordInfo> suggestions) { - // Search for the lowercase version of the word only, because that's where bigrams - // store their sons. - final Node prevWord = searchNode(mRoots, previousWord.toLowerCase(), 0, - previousWord.length()); - if (prevWord != null && prevWord.mNGrams != null) { - reverseLookUp(prevWord.mNGrams, suggestions); - } - } - - // Local to reverseLookUp, but do not allocate each time. - private final char[] mLookedUpString = new char[Constants.DICTIONARY_MAX_WORD_LENGTH]; - - /** - * reverseLookUp retrieves the full word given a list of terminal nodes and adds those words - * to the suggestions list passed as an argument. - * @param terminalNodes list of terminal nodes we want to add - * @param suggestions the suggestion collection to add the word to - */ - private void reverseLookUp(final LinkedList<NextWord> terminalNodes, - final ArrayList<SuggestedWordInfo> suggestions) { - Node node; - int freq; - for (NextWord nextWord : terminalNodes) { - node = nextWord.getWordNode(); - freq = nextWord.getFrequency(); - int index = Constants.DICTIONARY_MAX_WORD_LENGTH; - do { - --index; - mLookedUpString[index] = node.mCode; - node = node.mParent; - } while (node != null && index > 0); - - // If node is null, we have a word longer than MAX_WORD_LENGTH in the dictionary. - // It's a little unclear how this can happen, but just in case it does it's safer - // to ignore the word in this case. - if (freq >= 0 && node == null) { - suggestions.add(new SuggestedWordInfo(new String(mLookedUpString, index, - Constants.DICTIONARY_MAX_WORD_LENGTH - index), - freq, SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); - } - } - } - - /** - * Recursively search for the terminal node of the word. - * - * One iteration takes the full word to search for and the current index of the recursion. - * - * @param children the node of the trie to search under. - * @param word the word to search for. Only read [offset..length] so there may be trailing chars - * @param offset the index in {@code word} this recursion should operate on. - * @param length the length of the input word. - * @return Returns the terminal node of the word if the word exists - */ - private Node searchNode(final NodeArray children, final CharSequence word, final int offset, - final int length) { - final int count = children.mLength; - final char currentChar = word.charAt(offset); - for (int j = 0; j < count; j++) { - final Node node = children.mData[j]; - if (node.mCode == currentChar) { - if (offset == length - 1) { - if (node.mTerminal) { - return node; - } - } else { - if (node.mChildren != null) { - Node returnNode = searchNode(node.mChildren, word, offset + 1, length); - if (returnNode != null) return returnNode; - } - } - } - } - return null; - } - - public void clearDictionary() { - mRoots = new NodeArray(); - } - - private static char toLowerCase(final char c) { - char baseChar = c; - if (c < BASE_CHARS.length) { - baseChar = BASE_CHARS[c]; - } - if (baseChar >= 'A' && baseChar <= 'Z') { - return (char)(baseChar | 32); - } else if (baseChar > 127) { - return Character.toLowerCase(baseChar); - } - return baseChar; - } - - /** - * Table mapping most combined Latin, Greek, and Cyrillic characters - * to their base characters. If c is in range, BASE_CHARS[c] == c - * if c is not a combined character, or the base character if it - * is combined. - * - * cf. native/jni/src/utils/char_utils.cpp - */ - private static final char BASE_CHARS[] = { - /* U+0000 */ 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, - /* U+0008 */ 0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F, - /* U+0010 */ 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017, - /* U+0018 */ 0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F, - /* U+0020 */ 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, - /* U+0028 */ 0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F, - /* U+0030 */ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, - /* U+0038 */ 0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F, - /* U+0040 */ 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, - /* U+0048 */ 0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, - /* U+0050 */ 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, - /* U+0058 */ 0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F, - /* U+0060 */ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, - /* U+0068 */ 0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F, - /* U+0070 */ 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, - /* U+0078 */ 0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F, - /* U+0080 */ 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, - /* U+0088 */ 0x0088, 0x0089, 0x008A, 0x008B, 0x008C, 0x008D, 0x008E, 0x008F, - /* U+0090 */ 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, - /* U+0098 */ 0x0098, 0x0099, 0x009A, 0x009B, 0x009C, 0x009D, 0x009E, 0x009F, - /* U+00A0 */ 0x0020, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7, - /* U+00A8 */ 0x0020, 0x00A9, 0x0061, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x0020, - /* U+00B0 */ 0x00B0, 0x00B1, 0x0032, 0x0033, 0x0020, 0x03BC, 0x00B6, 0x00B7, - /* U+00B8 */ 0x0020, 0x0031, 0x006F, 0x00BB, 0x0031, 0x0031, 0x0033, 0x00BF, - /* U+00C0 */ 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00C6, 0x0043, - /* U+00C8 */ 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049, - /* U+00D0 */ 0x00D0, 0x004E, 0x004F, 0x004F, 0x004F, 0x004F, 0x004F, 0x00D7, - /* U+00D8 */ 0x004F, 0x0055, 0x0055, 0x0055, 0x0055, 0x0059, 0x00DE, 0x0073, - // U+00D8: Manually changed from 00D8 to 004F - // TODO: Check if it's really acceptable to consider Ø a diacritical variant of O - // U+00DF: Manually changed from 00DF to 0073 - /* U+00E0 */ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00E6, 0x0063, - /* U+00E8 */ 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069, - /* U+00F0 */ 0x00F0, 0x006E, 0x006F, 0x006F, 0x006F, 0x006F, 0x006F, 0x00F7, - /* U+00F8 */ 0x006F, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00FE, 0x0079, - // U+00F8: Manually changed from 00F8 to 006F - // TODO: Check if it's really acceptable to consider ø a diacritical variant of o - /* U+0100 */ 0x0041, 0x0061, 0x0041, 0x0061, 0x0041, 0x0061, 0x0043, 0x0063, - /* U+0108 */ 0x0043, 0x0063, 0x0043, 0x0063, 0x0043, 0x0063, 0x0044, 0x0064, - /* U+0110 */ 0x0110, 0x0111, 0x0045, 0x0065, 0x0045, 0x0065, 0x0045, 0x0065, - /* U+0118 */ 0x0045, 0x0065, 0x0045, 0x0065, 0x0047, 0x0067, 0x0047, 0x0067, - /* U+0120 */ 0x0047, 0x0067, 0x0047, 0x0067, 0x0048, 0x0068, 0x0126, 0x0127, - /* U+0128 */ 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, - /* U+0130 */ 0x0049, 0x0131, 0x0049, 0x0069, 0x004A, 0x006A, 0x004B, 0x006B, - /* U+0138 */ 0x0138, 0x004C, 0x006C, 0x004C, 0x006C, 0x004C, 0x006C, 0x004C, - /* U+0140 */ 0x006C, 0x004C, 0x006C, 0x004E, 0x006E, 0x004E, 0x006E, 0x004E, - // U+0141: Manually changed from 0141 to 004C - // U+0142: Manually changed from 0142 to 006C - /* U+0148 */ 0x006E, 0x02BC, 0x014A, 0x014B, 0x004F, 0x006F, 0x004F, 0x006F, - /* U+0150 */ 0x004F, 0x006F, 0x0152, 0x0153, 0x0052, 0x0072, 0x0052, 0x0072, - /* U+0158 */ 0x0052, 0x0072, 0x0053, 0x0073, 0x0053, 0x0073, 0x0053, 0x0073, - /* U+0160 */ 0x0053, 0x0073, 0x0054, 0x0074, 0x0054, 0x0074, 0x0166, 0x0167, - /* U+0168 */ 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, - /* U+0170 */ 0x0055, 0x0075, 0x0055, 0x0075, 0x0057, 0x0077, 0x0059, 0x0079, - /* U+0178 */ 0x0059, 0x005A, 0x007A, 0x005A, 0x007A, 0x005A, 0x007A, 0x0073, - /* U+0180 */ 0x0180, 0x0181, 0x0182, 0x0183, 0x0184, 0x0185, 0x0186, 0x0187, - /* U+0188 */ 0x0188, 0x0189, 0x018A, 0x018B, 0x018C, 0x018D, 0x018E, 0x018F, - /* U+0190 */ 0x0190, 0x0191, 0x0192, 0x0193, 0x0194, 0x0195, 0x0196, 0x0197, - /* U+0198 */ 0x0198, 0x0199, 0x019A, 0x019B, 0x019C, 0x019D, 0x019E, 0x019F, - /* U+01A0 */ 0x004F, 0x006F, 0x01A2, 0x01A3, 0x01A4, 0x01A5, 0x01A6, 0x01A7, - /* U+01A8 */ 0x01A8, 0x01A9, 0x01AA, 0x01AB, 0x01AC, 0x01AD, 0x01AE, 0x0055, - /* U+01B0 */ 0x0075, 0x01B1, 0x01B2, 0x01B3, 0x01B4, 0x01B5, 0x01B6, 0x01B7, - /* U+01B8 */ 0x01B8, 0x01B9, 0x01BA, 0x01BB, 0x01BC, 0x01BD, 0x01BE, 0x01BF, - /* U+01C0 */ 0x01C0, 0x01C1, 0x01C2, 0x01C3, 0x0044, 0x0044, 0x0064, 0x004C, - /* U+01C8 */ 0x004C, 0x006C, 0x004E, 0x004E, 0x006E, 0x0041, 0x0061, 0x0049, - /* U+01D0 */ 0x0069, 0x004F, 0x006F, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, - // U+01D5: Manually changed from 00DC to 0055 - // U+01D6: Manually changed from 00FC to 0075 - // U+01D7: Manually changed from 00DC to 0055 - /* U+01D8 */ 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x01DD, 0x0041, 0x0061, - // U+01D8: Manually changed from 00FC to 0075 - // U+01D9: Manually changed from 00DC to 0055 - // U+01DA: Manually changed from 00FC to 0075 - // U+01DB: Manually changed from 00DC to 0055 - // U+01DC: Manually changed from 00FC to 0075 - // U+01DE: Manually changed from 00C4 to 0041 - // U+01DF: Manually changed from 00E4 to 0061 - /* U+01E0 */ 0x0041, 0x0061, 0x00C6, 0x00E6, 0x01E4, 0x01E5, 0x0047, 0x0067, - // U+01E0: Manually changed from 0226 to 0041 - // U+01E1: Manually changed from 0227 to 0061 - /* U+01E8 */ 0x004B, 0x006B, 0x004F, 0x006F, 0x004F, 0x006F, 0x01B7, 0x0292, - // U+01EC: Manually changed from 01EA to 004F - // U+01ED: Manually changed from 01EB to 006F - /* U+01F0 */ 0x006A, 0x0044, 0x0044, 0x0064, 0x0047, 0x0067, 0x01F6, 0x01F7, - /* U+01F8 */ 0x004E, 0x006E, 0x0041, 0x0061, 0x00C6, 0x00E6, 0x004F, 0x006F, - // U+01FA: Manually changed from 00C5 to 0041 - // U+01FB: Manually changed from 00E5 to 0061 - // U+01FE: Manually changed from 00D8 to 004F - // TODO: Check if it's really acceptable to consider Ø a diacritical variant of O - // U+01FF: Manually changed from 00F8 to 006F - // TODO: Check if it's really acceptable to consider ø a diacritical variant of o - /* U+0200 */ 0x0041, 0x0061, 0x0041, 0x0061, 0x0045, 0x0065, 0x0045, 0x0065, - /* U+0208 */ 0x0049, 0x0069, 0x0049, 0x0069, 0x004F, 0x006F, 0x004F, 0x006F, - /* U+0210 */ 0x0052, 0x0072, 0x0052, 0x0072, 0x0055, 0x0075, 0x0055, 0x0075, - /* U+0218 */ 0x0053, 0x0073, 0x0054, 0x0074, 0x021C, 0x021D, 0x0048, 0x0068, - /* U+0220 */ 0x0220, 0x0221, 0x0222, 0x0223, 0x0224, 0x0225, 0x0041, 0x0061, - /* U+0228 */ 0x0045, 0x0065, 0x004F, 0x006F, 0x004F, 0x006F, 0x004F, 0x006F, - // U+022A: Manually changed from 00D6 to 004F - // U+022B: Manually changed from 00F6 to 006F - // U+022C: Manually changed from 00D5 to 004F - // U+022D: Manually changed from 00F5 to 006F - /* U+0230 */ 0x004F, 0x006F, 0x0059, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237, - // U+0230: Manually changed from 022E to 004F - // U+0231: Manually changed from 022F to 006F - /* U+0238 */ 0x0238, 0x0239, 0x023A, 0x023B, 0x023C, 0x023D, 0x023E, 0x023F, - /* U+0240 */ 0x0240, 0x0241, 0x0242, 0x0243, 0x0244, 0x0245, 0x0246, 0x0247, - /* U+0248 */ 0x0248, 0x0249, 0x024A, 0x024B, 0x024C, 0x024D, 0x024E, 0x024F, - /* U+0250 */ 0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257, - /* U+0258 */ 0x0258, 0x0259, 0x025A, 0x025B, 0x025C, 0x025D, 0x025E, 0x025F, - /* U+0260 */ 0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267, - /* U+0268 */ 0x0268, 0x0269, 0x026A, 0x026B, 0x026C, 0x026D, 0x026E, 0x026F, - /* U+0270 */ 0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277, - /* U+0278 */ 0x0278, 0x0279, 0x027A, 0x027B, 0x027C, 0x027D, 0x027E, 0x027F, - /* U+0280 */ 0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287, - /* U+0288 */ 0x0288, 0x0289, 0x028A, 0x028B, 0x028C, 0x028D, 0x028E, 0x028F, - /* U+0290 */ 0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297, - /* U+0298 */ 0x0298, 0x0299, 0x029A, 0x029B, 0x029C, 0x029D, 0x029E, 0x029F, - /* U+02A0 */ 0x02A0, 0x02A1, 0x02A2, 0x02A3, 0x02A4, 0x02A5, 0x02A6, 0x02A7, - /* U+02A8 */ 0x02A8, 0x02A9, 0x02AA, 0x02AB, 0x02AC, 0x02AD, 0x02AE, 0x02AF, - /* U+02B0 */ 0x0068, 0x0266, 0x006A, 0x0072, 0x0279, 0x027B, 0x0281, 0x0077, - /* U+02B8 */ 0x0079, 0x02B9, 0x02BA, 0x02BB, 0x02BC, 0x02BD, 0x02BE, 0x02BF, - /* U+02C0 */ 0x02C0, 0x02C1, 0x02C2, 0x02C3, 0x02C4, 0x02C5, 0x02C6, 0x02C7, - /* U+02C8 */ 0x02C8, 0x02C9, 0x02CA, 0x02CB, 0x02CC, 0x02CD, 0x02CE, 0x02CF, - /* U+02D0 */ 0x02D0, 0x02D1, 0x02D2, 0x02D3, 0x02D4, 0x02D5, 0x02D6, 0x02D7, - /* U+02D8 */ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02DE, 0x02DF, - /* U+02E0 */ 0x0263, 0x006C, 0x0073, 0x0078, 0x0295, 0x02E5, 0x02E6, 0x02E7, - /* U+02E8 */ 0x02E8, 0x02E9, 0x02EA, 0x02EB, 0x02EC, 0x02ED, 0x02EE, 0x02EF, - /* U+02F0 */ 0x02F0, 0x02F1, 0x02F2, 0x02F3, 0x02F4, 0x02F5, 0x02F6, 0x02F7, - /* U+02F8 */ 0x02F8, 0x02F9, 0x02FA, 0x02FB, 0x02FC, 0x02FD, 0x02FE, 0x02FF, - /* U+0300 */ 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307, - /* U+0308 */ 0x0308, 0x0309, 0x030A, 0x030B, 0x030C, 0x030D, 0x030E, 0x030F, - /* U+0310 */ 0x0310, 0x0311, 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317, - /* U+0318 */ 0x0318, 0x0319, 0x031A, 0x031B, 0x031C, 0x031D, 0x031E, 0x031F, - /* U+0320 */ 0x0320, 0x0321, 0x0322, 0x0323, 0x0324, 0x0325, 0x0326, 0x0327, - /* U+0328 */ 0x0328, 0x0329, 0x032A, 0x032B, 0x032C, 0x032D, 0x032E, 0x032F, - /* U+0330 */ 0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, 0x0336, 0x0337, - /* U+0338 */ 0x0338, 0x0339, 0x033A, 0x033B, 0x033C, 0x033D, 0x033E, 0x033F, - /* U+0340 */ 0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x0345, 0x0346, 0x0347, - /* U+0348 */ 0x0348, 0x0349, 0x034A, 0x034B, 0x034C, 0x034D, 0x034E, 0x034F, - /* U+0350 */ 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357, - /* U+0358 */ 0x0358, 0x0359, 0x035A, 0x035B, 0x035C, 0x035D, 0x035E, 0x035F, - /* U+0360 */ 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, - /* U+0368 */ 0x0368, 0x0369, 0x036A, 0x036B, 0x036C, 0x036D, 0x036E, 0x036F, - /* U+0370 */ 0x0370, 0x0371, 0x0372, 0x0373, 0x02B9, 0x0375, 0x0376, 0x0377, - /* U+0378 */ 0x0378, 0x0379, 0x0020, 0x037B, 0x037C, 0x037D, 0x003B, 0x037F, - /* U+0380 */ 0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x00A8, 0x0391, 0x00B7, - /* U+0388 */ 0x0395, 0x0397, 0x0399, 0x038B, 0x039F, 0x038D, 0x03A5, 0x03A9, - /* U+0390 */ 0x03CA, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, - /* U+0398 */ 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, - /* U+03A0 */ 0x03A0, 0x03A1, 0x03A2, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, - /* U+03A8 */ 0x03A8, 0x03A9, 0x0399, 0x03A5, 0x03B1, 0x03B5, 0x03B7, 0x03B9, - /* U+03B0 */ 0x03CB, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, - /* U+03B8 */ 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, - /* U+03C0 */ 0x03C0, 0x03C1, 0x03C2, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, - /* U+03C8 */ 0x03C8, 0x03C9, 0x03B9, 0x03C5, 0x03BF, 0x03C5, 0x03C9, 0x03CF, - /* U+03D0 */ 0x03B2, 0x03B8, 0x03A5, 0x03D2, 0x03D2, 0x03C6, 0x03C0, 0x03D7, - /* U+03D8 */ 0x03D8, 0x03D9, 0x03DA, 0x03DB, 0x03DC, 0x03DD, 0x03DE, 0x03DF, - /* U+03E0 */ 0x03E0, 0x03E1, 0x03E2, 0x03E3, 0x03E4, 0x03E5, 0x03E6, 0x03E7, - /* U+03E8 */ 0x03E8, 0x03E9, 0x03EA, 0x03EB, 0x03EC, 0x03ED, 0x03EE, 0x03EF, - /* U+03F0 */ 0x03BA, 0x03C1, 0x03C2, 0x03F3, 0x0398, 0x03B5, 0x03F6, 0x03F7, - /* U+03F8 */ 0x03F8, 0x03A3, 0x03FA, 0x03FB, 0x03FC, 0x03FD, 0x03FE, 0x03FF, - /* U+0400 */ 0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406, - /* U+0408 */ 0x0408, 0x0409, 0x040A, 0x040B, 0x041A, 0x0418, 0x0423, 0x040F, - /* U+0410 */ 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417, - /* U+0418 */ 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F, - // U+0419: Manually changed from 0418 to 0419 - /* U+0420 */ 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, - /* U+0428 */ 0x0428, 0x0429, 0x042C, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F, - // U+042A: Manually changed from 042A to 042C - /* U+0430 */ 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, - /* U+0438 */ 0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F, - // U+0439: Manually changed from 0438 to 0439 - /* U+0440 */ 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, - /* U+0448 */ 0x0448, 0x0449, 0x044C, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F, - // U+044A: Manually changed from 044A to 044C - /* U+0450 */ 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456, - /* U+0458 */ 0x0458, 0x0459, 0x045A, 0x045B, 0x043A, 0x0438, 0x0443, 0x045F, - /* U+0460 */ 0x0460, 0x0461, 0x0462, 0x0463, 0x0464, 0x0465, 0x0466, 0x0467, - /* U+0468 */ 0x0468, 0x0469, 0x046A, 0x046B, 0x046C, 0x046D, 0x046E, 0x046F, - /* U+0470 */ 0x0470, 0x0471, 0x0472, 0x0473, 0x0474, 0x0475, 0x0474, 0x0475, - /* U+0478 */ 0x0478, 0x0479, 0x047A, 0x047B, 0x047C, 0x047D, 0x047E, 0x047F, - /* U+0480 */ 0x0480, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487, - /* U+0488 */ 0x0488, 0x0489, 0x048A, 0x048B, 0x048C, 0x048D, 0x048E, 0x048F, - /* U+0490 */ 0x0490, 0x0491, 0x0492, 0x0493, 0x0494, 0x0495, 0x0496, 0x0497, - /* U+0498 */ 0x0498, 0x0499, 0x049A, 0x049B, 0x049C, 0x049D, 0x049E, 0x049F, - /* U+04A0 */ 0x04A0, 0x04A1, 0x04A2, 0x04A3, 0x04A4, 0x04A5, 0x04A6, 0x04A7, - /* U+04A8 */ 0x04A8, 0x04A9, 0x04AA, 0x04AB, 0x04AC, 0x04AD, 0x04AE, 0x04AF, - /* U+04B0 */ 0x04B0, 0x04B1, 0x04B2, 0x04B3, 0x04B4, 0x04B5, 0x04B6, 0x04B7, - /* U+04B8 */ 0x04B8, 0x04B9, 0x04BA, 0x04BB, 0x04BC, 0x04BD, 0x04BE, 0x04BF, - /* U+04C0 */ 0x04C0, 0x0416, 0x0436, 0x04C3, 0x04C4, 0x04C5, 0x04C6, 0x04C7, - /* U+04C8 */ 0x04C8, 0x04C9, 0x04CA, 0x04CB, 0x04CC, 0x04CD, 0x04CE, 0x04CF, - /* U+04D0 */ 0x0410, 0x0430, 0x0410, 0x0430, 0x04D4, 0x04D5, 0x0415, 0x0435, - /* U+04D8 */ 0x04D8, 0x04D9, 0x04D8, 0x04D9, 0x0416, 0x0436, 0x0417, 0x0437, - /* U+04E0 */ 0x04E0, 0x04E1, 0x0418, 0x0438, 0x0418, 0x0438, 0x041E, 0x043E, - /* U+04E8 */ 0x04E8, 0x04E9, 0x04E8, 0x04E9, 0x042D, 0x044D, 0x0423, 0x0443, - /* U+04F0 */ 0x0423, 0x0443, 0x0423, 0x0443, 0x0427, 0x0447, 0x04F6, 0x04F7, - /* U+04F8 */ 0x042B, 0x044B, 0x04FA, 0x04FB, 0x04FC, 0x04FD, 0x04FE, 0x04FF, - }; -} diff --git a/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java b/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java new file mode 100644 index 000000000..567087c81 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/ImportantNoticeDialog.java @@ -0,0 +1,78 @@ +/* + * 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.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; + +import com.android.inputmethod.latin.utils.DialogUtils; +import com.android.inputmethod.latin.utils.ImportantNoticeUtils; + +/** + * The dialog box that shows the important notice contents. + */ +public final class ImportantNoticeDialog extends AlertDialog implements OnClickListener { + public interface ImportantNoticeDialogListener { + public void onUserAcknowledgmentOfImportantNoticeDialog(final int nextVersion); + public void onClickSettingsOfImportantNoticeDialog(final int nextVersion); + } + + private final ImportantNoticeDialogListener mListener; + private final int mNextImportantNoticeVersion; + + public ImportantNoticeDialog( + final Context context, final ImportantNoticeDialogListener listener) { + super(DialogUtils.getPlatformDialogThemeContext(context)); + mListener = listener; + mNextImportantNoticeVersion = ImportantNoticeUtils.getNextImportantNoticeVersion(context); + setMessage(ImportantNoticeUtils.getNextImportantNoticeContents(context)); + // Create buttons and set listeners. + setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok), this); + if (shouldHaveSettingsButton()) { + setButton(BUTTON_NEGATIVE, context.getString(R.string.go_to_settings), this); + } + // This dialog is cancelable by pressing back key. See {@link #onBackPress()}. + setCancelable(true /* cancelable */); + setCanceledOnTouchOutside(false /* cancelable */); + } + + private boolean shouldHaveSettingsButton() { + return mNextImportantNoticeVersion + == ImportantNoticeUtils.VERSION_TO_ENABLE_PERSONALIZED_SUGGESTIONS; + } + + private void userAcknowledged() { + ImportantNoticeUtils.updateLastImportantNoticeVersion(getContext()); + mListener.onUserAcknowledgmentOfImportantNoticeDialog(mNextImportantNoticeVersion); + } + + @Override + public void onClick(final DialogInterface dialog, final int which) { + if (shouldHaveSettingsButton() && which == BUTTON_NEGATIVE) { + mListener.onClickSettingsOfImportantNoticeDialog(mNextImportantNoticeVersion); + } + userAcknowledged(); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + userAcknowledged(); + } +} diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java index 8caf6f17f..726b3d141 100644 --- a/java/src/com/android/inputmethod/latin/InputAttributes.java +++ b/java/src/com/android/inputmethod/latin/InputAttributes.java @@ -20,22 +20,29 @@ import android.text.InputType; import android.util.Log; import android.view.inputmethod.EditorInfo; +import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.InputTypeUtils; import com.android.inputmethod.latin.utils.StringUtils; +import java.util.ArrayList; +import java.util.Arrays; + /** * Class to hold attributes of the input field. */ public final class InputAttributes { private final String TAG = InputAttributes.class.getSimpleName(); + final public String mTargetApplicationPackageName; final public boolean mInputTypeNoAutoCorrect; + final public boolean mIsPasswordField; final public boolean mIsSettingsSuggestionStripOn; final public boolean mApplicationSpecifiedCompletionOn; final public boolean mShouldInsertSpacesAutomatically; final private int mInputType; public InputAttributes(final EditorInfo editorInfo, final boolean isFullscreenMode) { + mTargetApplicationPackageName = null != editorInfo ? editorInfo.packageName : null; final int inputType = null != editorInfo ? editorInfo.inputType : 0; final int inputClass = inputType & InputType.TYPE_MASK_CLASS; mInputType = inputType; @@ -52,55 +59,50 @@ public final class InputAttributes { } else if (inputClass == 0) { // TODO: is this check still necessary? Log.w(TAG, String.format("Unexpected input class: inputType=0x%08x" - + " imeOptions=0x%08x", - inputType, editorInfo.imeOptions)); + + " imeOptions=0x%08x", inputType, editorInfo.imeOptions)); } + mIsPasswordField = false; mIsSettingsSuggestionStripOn = false; mInputTypeNoAutoCorrect = false; mApplicationSpecifiedCompletionOn = false; mShouldInsertSpacesAutomatically = false; - } else { - final int variation = inputType & InputType.TYPE_MASK_VARIATION; - final boolean flagNoSuggestions = - 0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); - final boolean flagMultiLine = - 0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE); - final boolean flagAutoCorrect = - 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); - final boolean flagAutoComplete = - 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); - - // TODO: Have a helper method in InputTypeUtils - // Make sure that passwords are not displayed in {@link SuggestionStripView}. - if (InputTypeUtils.isPasswordInputType(inputType) - || InputTypeUtils.isVisiblePasswordInputType(inputType) - || InputTypeUtils.isEmailVariation(variation) - || InputType.TYPE_TEXT_VARIATION_URI == variation - || InputType.TYPE_TEXT_VARIATION_FILTER == variation - || flagNoSuggestions - || flagAutoComplete) { - mIsSettingsSuggestionStripOn = false; - } else { - mIsSettingsSuggestionStripOn = true; - } + return; + } + // inputClass == InputType.TYPE_CLASS_TEXT + final int variation = inputType & InputType.TYPE_MASK_VARIATION; + final boolean flagNoSuggestions = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + final boolean flagMultiLine = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE); + final boolean flagAutoCorrect = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); + final boolean flagAutoComplete = + 0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); - mShouldInsertSpacesAutomatically = InputTypeUtils.isAutoSpaceFriendlyType(inputType); - - // 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. - // If it's not multiline and the autoCorrect flag is not set, then don't correct - if ((variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT - && !flagAutoCorrect) - || flagNoSuggestions - || (!flagAutoCorrect && !flagMultiLine)) { - mInputTypeNoAutoCorrect = true; - } else { - mInputTypeNoAutoCorrect = false; - } + mIsPasswordField = InputTypeUtils.isPasswordInputType(inputType) + || InputTypeUtils.isVisiblePasswordInputType(inputType); + // TODO: Have a helper method in InputTypeUtils + // Make sure that passwords are not displayed in {@link SuggestionStripView}. + final boolean noSuggestionStrip = mIsPasswordField + || InputTypeUtils.isEmailVariation(variation) + || InputType.TYPE_TEXT_VARIATION_URI == variation + || InputType.TYPE_TEXT_VARIATION_FILTER == variation + || flagNoSuggestions + || flagAutoComplete; + mIsSettingsSuggestionStripOn = !noSuggestionStrip; - mApplicationSpecifiedCompletionOn = flagAutoComplete && isFullscreenMode; - } + mShouldInsertSpacesAutomatically = InputTypeUtils.isAutoSpaceFriendlyType(inputType); + + // 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. + // If it's not multiline and the autoCorrect flag is not set, then don't correct + mInputTypeNoAutoCorrect = + (variation == InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT && !flagAutoCorrect) + || flagNoSuggestions + || (!flagAutoCorrect && !flagMultiLine); + + mApplicationSpecifiedCompletionOn = flagAutoComplete && isFullscreenMode; } public boolean isTypeNull() { @@ -113,99 +115,144 @@ public final class InputAttributes { @SuppressWarnings("unused") private void dumpFlags(final int inputType) { - Log.i(TAG, "Input class:"); final int inputClass = inputType & InputType.TYPE_MASK_CLASS; - if (inputClass == InputType.TYPE_CLASS_TEXT) - Log.i(TAG, " TYPE_CLASS_TEXT"); - if (inputClass == InputType.TYPE_CLASS_PHONE) - Log.i(TAG, " TYPE_CLASS_PHONE"); - if (inputClass == InputType.TYPE_CLASS_NUMBER) - Log.i(TAG, " TYPE_CLASS_NUMBER"); - if (inputClass == InputType.TYPE_CLASS_DATETIME) - Log.i(TAG, " TYPE_CLASS_DATETIME"); - Log.i(TAG, "Variation:"); - switch (InputType.TYPE_MASK_VARIATION & inputType) { - case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS: - Log.i(TAG, " TYPE_TEXT_VARIATION_EMAIL_ADDRESS"); - break; - case InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT: - Log.i(TAG, " TYPE_TEXT_VARIATION_EMAIL_SUBJECT"); - break; - case InputType.TYPE_TEXT_VARIATION_FILTER: - Log.i(TAG, " TYPE_TEXT_VARIATION_FILTER"); - break; - case InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE: - Log.i(TAG, " TYPE_TEXT_VARIATION_LONG_MESSAGE"); - break; - case InputType.TYPE_TEXT_VARIATION_NORMAL: - Log.i(TAG, " TYPE_TEXT_VARIATION_NORMAL"); - break; - case InputType.TYPE_TEXT_VARIATION_PASSWORD: - Log.i(TAG, " TYPE_TEXT_VARIATION_PASSWORD"); - break; - case InputType.TYPE_TEXT_VARIATION_PERSON_NAME: - Log.i(TAG, " TYPE_TEXT_VARIATION_PERSON_NAME"); - break; - case InputType.TYPE_TEXT_VARIATION_PHONETIC: - Log.i(TAG, " TYPE_TEXT_VARIATION_PHONETIC"); - break; - case InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS: - Log.i(TAG, " TYPE_TEXT_VARIATION_POSTAL_ADDRESS"); - break; - case InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE: - Log.i(TAG, " TYPE_TEXT_VARIATION_SHORT_MESSAGE"); - break; - case InputType.TYPE_TEXT_VARIATION_URI: - Log.i(TAG, " TYPE_TEXT_VARIATION_URI"); - break; - case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: - Log.i(TAG, " TYPE_TEXT_VARIATION_VISIBLE_PASSWORD"); - break; - case InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT: - Log.i(TAG, " TYPE_TEXT_VARIATION_WEB_EDIT_TEXT"); - break; - case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: - Log.i(TAG, " TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS"); - break; - case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD: - Log.i(TAG, " TYPE_TEXT_VARIATION_WEB_PASSWORD"); - break; - default: - Log.i(TAG, " Unknown variation"); - break; + final String inputClassString = toInputClassString(inputClass); + final String variationString = toVariationString( + inputClass, inputType & InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); + final String flagsString = toFlagsString(inputType & InputType.TYPE_MASK_FLAGS); + Log.i(TAG, "Input class: " + inputClassString); + Log.i(TAG, "Variation: " + variationString); + Log.i(TAG, "Flags: " + flagsString); + } + + private static String toInputClassString(final int inputClass) { + switch (inputClass) { + case InputType.TYPE_CLASS_TEXT: + return "TYPE_CLASS_TEXT"; + case InputType.TYPE_CLASS_PHONE: + return "TYPE_CLASS_PHONE"; + case InputType.TYPE_CLASS_NUMBER: + return "TYPE_CLASS_NUMBER"; + case InputType.TYPE_CLASS_DATETIME: + return "TYPE_CLASS_DATETIME"; + default: + return String.format("unknownInputClass<0x%08x>", inputClass); + } + } + + private static String toVariationString(final int inputClass, final int variation) { + switch (inputClass) { + case InputType.TYPE_CLASS_TEXT: + return toTextVariationString(variation); + case InputType.TYPE_CLASS_NUMBER: + return toNumberVariationString(variation); + case InputType.TYPE_CLASS_DATETIME: + return toDatetimeVariationString(variation); + default: + return ""; } - Log.i(TAG, "Flags:"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS)) - Log.i(TAG, " TYPE_TEXT_FLAG_NO_SUGGESTIONS"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE)) - Log.i(TAG, " TYPE_TEXT_FLAG_MULTI_LINE"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE)) - Log.i(TAG, " TYPE_TEXT_FLAG_IME_MULTI_LINE"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS)) - Log.i(TAG, " TYPE_TEXT_FLAG_CAP_WORDS"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)) - Log.i(TAG, " TYPE_TEXT_FLAG_CAP_SENTENCES"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS)) - Log.i(TAG, " TYPE_TEXT_FLAG_CAP_CHARACTERS"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT)) - Log.i(TAG, " TYPE_TEXT_FLAG_AUTO_CORRECT"); - if (0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)) - Log.i(TAG, " TYPE_TEXT_FLAG_AUTO_COMPLETE"); + } + + private static String toTextVariationString(final int variation) { + switch (variation) { + case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS: + return " TYPE_TEXT_VARIATION_EMAIL_ADDRESS"; + case InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT: + return "TYPE_TEXT_VARIATION_EMAIL_SUBJECT"; + case InputType.TYPE_TEXT_VARIATION_FILTER: + return "TYPE_TEXT_VARIATION_FILTER"; + case InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE: + return "TYPE_TEXT_VARIATION_LONG_MESSAGE"; + case InputType.TYPE_TEXT_VARIATION_NORMAL: + return "TYPE_TEXT_VARIATION_NORMAL"; + case InputType.TYPE_TEXT_VARIATION_PASSWORD: + return "TYPE_TEXT_VARIATION_PASSWORD"; + case InputType.TYPE_TEXT_VARIATION_PERSON_NAME: + return "TYPE_TEXT_VARIATION_PERSON_NAME"; + case InputType.TYPE_TEXT_VARIATION_PHONETIC: + return "TYPE_TEXT_VARIATION_PHONETIC"; + case InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS: + return "TYPE_TEXT_VARIATION_POSTAL_ADDRESS"; + case InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE: + return "TYPE_TEXT_VARIATION_SHORT_MESSAGE"; + case InputType.TYPE_TEXT_VARIATION_URI: + return "TYPE_TEXT_VARIATION_URI"; + case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: + return "TYPE_TEXT_VARIATION_VISIBLE_PASSWORD"; + case InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT: + return "TYPE_TEXT_VARIATION_WEB_EDIT_TEXT"; + case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: + return "TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS"; + case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD: + return "TYPE_TEXT_VARIATION_WEB_PASSWORD"; + default: + return String.format("unknownVariation<0x%08x>", variation); + } + } + + private static String toNumberVariationString(final int variation) { + switch (variation) { + case InputType.TYPE_NUMBER_VARIATION_NORMAL: + return "TYPE_NUMBER_VARIATION_NORMAL"; + case InputType.TYPE_NUMBER_VARIATION_PASSWORD: + return "TYPE_NUMBER_VARIATION_PASSWORD"; + default: + return String.format("unknownVariation<0x%08x>", variation); + } + } + + private static String toDatetimeVariationString(final int variation) { + switch (variation) { + case InputType.TYPE_DATETIME_VARIATION_NORMAL: + return "TYPE_DATETIME_VARIATION_NORMAL"; + case InputType.TYPE_DATETIME_VARIATION_DATE: + return "TYPE_DATETIME_VARIATION_DATE"; + case InputType.TYPE_DATETIME_VARIATION_TIME: + return "TYPE_DATETIME_VARIATION_TIME"; + default: + return String.format("unknownVariation<0x%08x>", variation); + } + } + + private static String toFlagsString(final int flags) { + final ArrayList<String> flagsArray = CollectionUtils.newArrayList(); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS)) + flagsArray.add("TYPE_TEXT_FLAG_NO_SUGGESTIONS"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_MULTI_LINE)) + flagsArray.add("TYPE_TEXT_FLAG_MULTI_LINE"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE)) + flagsArray.add("TYPE_TEXT_FLAG_IME_MULTI_LINE"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_CAP_WORDS)) + flagsArray.add("TYPE_TEXT_FLAG_CAP_WORDS"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)) + flagsArray.add("TYPE_TEXT_FLAG_CAP_SENTENCES"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS)) + flagsArray.add("TYPE_TEXT_FLAG_CAP_CHARACTERS"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT)) + flagsArray.add("TYPE_TEXT_FLAG_AUTO_CORRECT"); + if (0 != (flags & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)) + flagsArray.add("TYPE_TEXT_FLAG_AUTO_COMPLETE"); + return flagsArray.isEmpty() ? "" : Arrays.toString(flagsArray.toArray()); } // Pretty print @Override public String toString() { - return "\n mInputTypeNoAutoCorrect = " + mInputTypeNoAutoCorrect - + "\n mIsSettingsSuggestionStripOn = " + mIsSettingsSuggestionStripOn - + "\n mApplicationSpecifiedCompletionOn = " + mApplicationSpecifiedCompletionOn; + return String.format( + "%s: inputType=0x%08x%s%s%s%s%s targetApp=%s\n", getClass().getSimpleName(), + mInputType, + (mInputTypeNoAutoCorrect ? " noAutoCorrect" : ""), + (mIsPasswordField ? " password" : ""), + (mIsSettingsSuggestionStripOn ? " suggestionStrip" : ""), + (mApplicationSpecifiedCompletionOn ? " appSpecified" : ""), + (mShouldInsertSpacesAutomatically ? " insertSpaces" : ""), + mTargetApplicationPackageName); } - public static boolean inPrivateImeOptions(String packageName, String key, - EditorInfo editorInfo) { + public static boolean inPrivateImeOptions(final String packageName, final String key, + final EditorInfo editorInfo) { if (editorInfo == null) return false; - final String findingKey = (packageName != null) ? packageName + "." + key - : key; + final String findingKey = (packageName != null) ? packageName + "." + key : key; return StringUtils.containsInCommaSplittableText(findingKey, editorInfo.privateImeOptions); } } diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java index 2e638aaf3..47bc6b078 100644 --- a/java/src/com/android/inputmethod/latin/InputPointers.java +++ b/java/src/com/android/inputmethod/latin/InputPointers.java @@ -16,14 +16,17 @@ package com.android.inputmethod.latin; +import android.util.Log; +import android.util.SparseIntArray; + import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.utils.ResizableIntArray; -import android.util.Log; - // TODO: This class is not thread-safe. public final class InputPointers { private static final String TAG = InputPointers.class.getSimpleName(); + private static final boolean DEBUG_TIME = false; + private final int mDefaultCapacity; private final ResizableIntArray mXCoordinates; private final ResizableIntArray mYCoordinates; @@ -38,11 +41,29 @@ public final class InputPointers { mTimes = new ResizableIntArray(defaultCapacity); } - public void addPointer(int index, int x, int y, int pointerId, int time) { - mXCoordinates.add(index, x); - mYCoordinates.add(index, y); - mPointerIds.add(index, pointerId); - mTimes.add(index, time); + private void fillWithLastTimeUntil(final int index) { + final int fromIndex = mTimes.getLength(); + // Fill the gap with the latest time. + // See {@link #getTime(int)} and {@link #isValidTimeStamps()}. + if (fromIndex <= 0) { + return; + } + final int fillLength = index - fromIndex + 1; + if (fillLength <= 0) { + return; + } + final int lastTime = mTimes.get(fromIndex - 1); + mTimes.fill(lastTime, fromIndex, fillLength); + } + + public void addPointerAt(int index, int x, int y, int pointerId, int time) { + mXCoordinates.addAt(index, x); + mYCoordinates.addAt(index, y); + mPointerIds.addAt(index, pointerId); + if (LatinImeLogger.sDBG || DEBUG_TIME) { + fillWithLastTimeUntil(index); + } + mTimes.addAt(index, time); } @UsedForTesting @@ -68,23 +89,6 @@ public final class InputPointers { } /** - * Append the pointers in the specified {@link InputPointers} to the end of this. - * @param src the source {@link InputPointers} to read the data from. - * @param startPos the starting index of the pointers in {@code src}. - * @param length the number of pointers to be appended. - */ - @UsedForTesting - void append(InputPointers src, int startPos, int length) { - if (length == 0) { - return; - } - mXCoordinates.append(src.mXCoordinates, startPos, length); - mYCoordinates.append(src.mYCoordinates, startPos, length); - mPointerIds.append(src.mPointerIds, startPos, length); - mTimes.append(src.mTimes, startPos, length); - } - - /** * Append the times, x-coordinates and y-coordinates in the specified {@link ResizableIntArray} * to the end of this. * @param pointerId the pointer id of the source. @@ -141,7 +145,7 @@ public final class InputPointers { } public int[] getTimes() { - if (LatinImeLogger.sDBG) { + if (LatinImeLogger.sDBG || DEBUG_TIME) { if (!isValidTimeStamps()) { throw new RuntimeException("Time stamps are invalid."); } @@ -157,14 +161,21 @@ public final class InputPointers { private boolean isValidTimeStamps() { final int[] times = mTimes.getPrimitiveArray(); - for (int i = 1; i < getPointerSize(); ++i) { - if (times[i] < times[i - 1]) { + final int[] pointerIds = mPointerIds.getPrimitiveArray(); + final SparseIntArray lastTimeOfPointers = new SparseIntArray(); + final int size = getPointerSize(); + for (int i = 0; i < size; ++i) { + final int pointerId = pointerIds[i]; + final int time = times[i]; + final int lastTime = lastTimeOfPointers.get(pointerId, time); + if (time < lastTime) { // dump - for (int j = 0; j < times.length; ++j) { + for (int j = 0; j < size; ++j) { Log.d(TAG, "--- (" + j + ") " + times[j]); } return false; } + lastTimeOfPointers.put(pointerId, time); } return true; } diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java index 81ccf83d8..ea7859e60 100644 --- a/java/src/com/android/inputmethod/latin/InputView.java +++ b/java/src/com/android/inputmethod/latin/InputView.java @@ -23,87 +23,210 @@ import android.view.MotionEvent; import android.view.View; import android.widget.LinearLayout; -public final class InputView extends LinearLayout { - private View mSuggestionStripView; - private View mKeyboardView; - private int mKeyboardTopPadding; +import com.android.inputmethod.keyboard.MainKeyboardView; +import com.android.inputmethod.latin.suggestions.MoreSuggestionsView; +import com.android.inputmethod.latin.suggestions.SuggestionStripView; - private boolean mIsForwardingEvent; +public final class InputView extends LinearLayout { private final Rect mInputViewRect = new Rect(); - private final Rect mEventForwardingRect = new Rect(); - private final Rect mEventReceivingRect = new Rect(); + private KeyboardTopPaddingForwarder mKeyboardTopPaddingForwarder; + private MoreSuggestionsViewCanceler mMoreSuggestionsViewCanceler; + private MotionEventForwarder<?, ?> mActiveForwarder; public InputView(final Context context, final AttributeSet attrs) { super(context, attrs, 0); } - public void setKeyboardGeometry(final int keyboardTopPadding) { - mKeyboardTopPadding = keyboardTopPadding; - } - @Override protected void onFinishInflate() { - mSuggestionStripView = findViewById(R.id.suggestion_strip_view); - mKeyboardView = findViewById(R.id.keyboard_view); + final SuggestionStripView suggestionStripView = + (SuggestionStripView)findViewById(R.id.suggestion_strip_view); + final MainKeyboardView mainKeyboardView = + (MainKeyboardView)findViewById(R.id.keyboard_view); + mKeyboardTopPaddingForwarder = new KeyboardTopPaddingForwarder( + mainKeyboardView, suggestionStripView); + mMoreSuggestionsViewCanceler = new MoreSuggestionsViewCanceler( + mainKeyboardView, suggestionStripView); + } + + public void setKeyboardTopPadding(final int keyboardTopPadding) { + mKeyboardTopPaddingForwarder.setKeyboardTopPadding(keyboardTopPadding); } @Override - public boolean dispatchTouchEvent(final MotionEvent me) { - if (mSuggestionStripView.getVisibility() != VISIBLE - || mKeyboardView.getVisibility() != VISIBLE) { - return super.dispatchTouchEvent(me); - } + public boolean onInterceptTouchEvent(final MotionEvent me) { + final Rect rect = mInputViewRect; + getGlobalVisibleRect(rect); + final int index = me.getActionIndex(); + final int x = (int)me.getX(index) + rect.left; + final int y = (int)me.getY(index) + rect.top; // The touch events that hit the top padding of keyboard should be forwarded to // {@link SuggestionStripView}. + if (mKeyboardTopPaddingForwarder.onInterceptTouchEvent(x, y, me)) { + mActiveForwarder = mKeyboardTopPaddingForwarder; + return true; + } + + // To cancel {@link MoreSuggestionsView}, we should intercept a touch event to + // {@link MainKeyboardView} and dismiss the {@link MoreSuggestionsView}. + if (mMoreSuggestionsViewCanceler.onInterceptTouchEvent(x, y, me)) { + mActiveForwarder = mMoreSuggestionsViewCanceler; + return true; + } + + mActiveForwarder = null; + return false; + } + + @Override + public boolean onTouchEvent(final MotionEvent me) { + if (mActiveForwarder == null) { + return super.onTouchEvent(me); + } + final Rect rect = mInputViewRect; - this.getGlobalVisibleRect(rect); - final int x = (int)me.getX() + rect.left; - final int y = (int)me.getY() + rect.top; + getGlobalVisibleRect(rect); + final int index = me.getActionIndex(); + final int x = (int)me.getX(index) + rect.left; + final int y = (int)me.getY(index) + rect.top; + return mActiveForwarder.onTouchEvent(x, y, me); + } - final Rect forwardingRect = mEventForwardingRect; - mKeyboardView.getGlobalVisibleRect(forwardingRect); - if (!mIsForwardingEvent && !forwardingRect.contains(x, y)) { - return super.dispatchTouchEvent(me); + /** + * This class forwards series of {@link MotionEvent}s from <code>SenderView</code> to + * <code>ReceiverView</code>. + * + * @param <SenderView> a {@link View} that may send a {@link MotionEvent} to <ReceiverView>. + * @param <ReceiverView> a {@link View} that receives forwarded {@link MotionEvent} from + * <SenderView>. + */ + private static abstract class + MotionEventForwarder<SenderView extends View, ReceiverView extends View> { + protected final SenderView mSenderView; + protected final ReceiverView mReceiverView; + + protected final Rect mEventSendingRect = new Rect(); + protected final Rect mEventReceivingRect = new Rect(); + + public MotionEventForwarder(final SenderView senderView, final ReceiverView receiverView) { + mSenderView = senderView; + mReceiverView = receiverView; } - final int forwardingLimitY = forwardingRect.top + mKeyboardTopPadding; - boolean sendToTarget = false; + // Return true if a touch event of global coordinate x, y needs to be forwarded. + protected abstract boolean needsToForward(final int x, final int y); - switch (me.getAction()) { - case MotionEvent.ACTION_DOWN: - if (y < forwardingLimitY) { - // This down event and further move and up events should be forwarded to the target. - mIsForwardingEvent = true; - sendToTarget = true; + // Translate global x-coordinate to <code>ReceiverView</code> local coordinate. + protected int translateX(final int x) { + return x - mEventReceivingRect.left; + } + + // Translate global y-coordinate to <code>ReceiverView</code> local coordinate. + protected int translateY(final int y) { + return y - mEventReceivingRect.top; + } + + // Callback when a {@link MotionEvent} is forwarded. + protected void onForwardingEvent(final MotionEvent me) {} + + // Returns true if a {@link MotionEvent} is needed to be forwarded to + // <code>ReceiverView</code>. Otherwise returns false. + public boolean onInterceptTouchEvent(final int x, final int y, final MotionEvent me) { + // Forwards a {link MotionEvent} only if both <code>SenderView</code> and + // <code>ReceiverView</code> are visible. + if (mSenderView.getVisibility() != View.VISIBLE || + mReceiverView.getVisibility() != View.VISIBLE) { + return false; + } + mSenderView.getGlobalVisibleRect(mEventSendingRect); + if (!mEventSendingRect.contains(x, y)) { + return false; } - break; - case MotionEvent.ACTION_MOVE: - sendToTarget = mIsForwardingEvent; - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - sendToTarget = mIsForwardingEvent; - mIsForwardingEvent = false; - break; - } - - if (!sendToTarget) { - return super.dispatchTouchEvent(me); - } - - final Rect receivingRect = mEventReceivingRect; - mSuggestionStripView.getGlobalVisibleRect(receivingRect); - final int translatedX = x - receivingRect.left; - final int translatedY; - if (y < forwardingLimitY) { - // The forwarded event should have coordinates that are inside of the target. - translatedY = Math.min(y - receivingRect.top, receivingRect.height() - 1); - } else { - translatedY = y - receivingRect.top; - } - me.setLocation(translatedX, translatedY); - mSuggestionStripView.dispatchTouchEvent(me); - return true; + + if (me.getActionMasked() == MotionEvent.ACTION_DOWN) { + // If the down event happens in the forwarding area, successive + // {@link MotionEvent}s should be forwarded to <code>ReceiverView</code>. + if (needsToForward(x, y)) { + return true; + } + } + + return false; + } + + // Returns true if a {@link MotionEvent} is forwarded to <code>ReceiverView</code>. + // Otherwise returns false. + public boolean onTouchEvent(final int x, final int y, final MotionEvent me) { + mReceiverView.getGlobalVisibleRect(mEventReceivingRect); + // Translate global coordinates to <code>ReceiverView</code> local coordinates. + me.setLocation(translateX(x), translateY(y)); + mReceiverView.dispatchTouchEvent(me); + onForwardingEvent(me); + return true; + } + } + + /** + * This class forwards {@link MotionEvent}s happened in the top padding of + * {@link MainKeyboardView} to {@link SuggestionStripView}. + */ + private static class KeyboardTopPaddingForwarder + extends MotionEventForwarder<MainKeyboardView, SuggestionStripView> { + private int mKeyboardTopPadding; + + public KeyboardTopPaddingForwarder(final MainKeyboardView mainKeyboardView, + final SuggestionStripView suggestionStripView) { + super(mainKeyboardView, suggestionStripView); + } + + public void setKeyboardTopPadding(final int keyboardTopPadding) { + mKeyboardTopPadding = keyboardTopPadding; + } + + private boolean isInKeyboardTopPadding(final int y) { + return y < mEventSendingRect.top + mKeyboardTopPadding; + } + + @Override + protected boolean needsToForward(final int x, final int y) { + return isInKeyboardTopPadding(y); + } + + @Override + protected int translateY(final int y) { + final int translatedY = super.translateY(y); + if (isInKeyboardTopPadding(y)) { + // The forwarded event should have coordinates that are inside of the target. + return Math.min(translatedY, mEventReceivingRect.height() - 1); + } + return translatedY; + } + } + + /** + * This class forwards {@link MotionEvent}s happened in the {@link MainKeyboardView} to + * {@link SuggestionStripView} when the {@link MoreSuggestionsView} is showing. + * {@link SuggestionStripView} dismisses {@link MoreSuggestionsView} when it receives any event + * outside of it. + */ + private static class MoreSuggestionsViewCanceler + extends MotionEventForwarder<MainKeyboardView, SuggestionStripView> { + public MoreSuggestionsViewCanceler(final MainKeyboardView mainKeyboardView, + final SuggestionStripView suggestionStripView) { + super(mainKeyboardView, suggestionStripView); + } + + @Override + protected boolean needsToForward(final int x, final int y) { + return mReceiverView.isShowingMoreSuggestionPanel() && mEventSendingRect.contains(x, y); + } + + @Override + protected void onForwardingEvent(final MotionEvent me) { + if (me.getActionMasked() == MotionEvent.ACTION_DOWN) { + mReceiverView.dismissMoreSuggestionsPanel(); + } + } } } diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java index 2e9280c77..232bf7407 100644 --- a/java/src/com/android/inputmethod/latin/LastComposedWord.java +++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java @@ -18,6 +18,10 @@ package com.android.inputmethod.latin; import android.text.TextUtils; +import com.android.inputmethod.event.Event; + +import java.util.ArrayList; + /** * This class encapsulates data about a word previously composed, but that has been * committed already. This is used for resuming suggestion, and cancel auto-correction. @@ -40,9 +44,9 @@ public final class LastComposedWord { public static final String NOT_A_SEPARATOR = ""; - public final int[] mPrimaryKeyCodes; + public final ArrayList<Event> mEvents; public final String mTypedWord; - public final String mCommittedWord; + public final CharSequence mCommittedWord; public final String mSeparatorString; public final String mPrevWord; public final int mCapitalizedMode; @@ -52,19 +56,20 @@ public final class LastComposedWord { private boolean mActive; public static final LastComposedWord NOT_A_COMPOSED_WORD = - new LastComposedWord(null, null, "", "", NOT_A_SEPARATOR, null, - WordComposer.CAPS_MODE_OFF); + new LastComposedWord(new ArrayList<Event>(), null, "", "", + NOT_A_SEPARATOR, null, WordComposer.CAPS_MODE_OFF); // Warning: this is using the passed objects as is and fully expects them to be // immutable. Do not fiddle with their contents after you passed them to this constructor. - public LastComposedWord(final int[] primaryKeyCodes, final InputPointers inputPointers, - final String typedWord, final String committedWord, final String separatorString, + public LastComposedWord(final ArrayList<Event> events, + final InputPointers inputPointers, final String typedWord, + final CharSequence committedWord, final String separatorString, final String prevWord, final int capitalizedMode) { - mPrimaryKeyCodes = primaryKeyCodes; if (inputPointers != null) { mInputPointers.copy(inputPointers); } mTypedWord = typedWord; + mEvents = new ArrayList<Event>(events); mCommittedWord = committedWord; mSeparatorString = separatorString; mActive = true; diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 77d07019f..81b02c396 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -25,10 +25,10 @@ import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.content.pm.PackageInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; @@ -36,39 +36,33 @@ import android.inputmethodservice.InputMethodService; import android.media.AudioManager; import android.net.ConnectivityManager; import android.os.Debug; -import android.os.Handler; -import android.os.HandlerThread; import android.os.IBinder; import android.os.Message; -import android.os.SystemClock; import android.preference.PreferenceManager; import android.text.InputType; import android.text.TextUtils; -import android.text.style.SuggestionSpan; import android.util.Log; -import android.util.Pair; import android.util.PrintWriterPrinter; import android.util.Printer; -import android.view.KeyCharacterMap; +import android.util.SparseArray; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.CompletionInfo; -import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.accessibility.AccessibilityUtils; import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.compat.AppWorkaroundsUtils; import com.android.inputmethod.compat.InputMethodServiceCompatUtils; -import com.android.inputmethod.compat.SuggestionSpanUtils; import com.android.inputmethod.dictionarypack.DictionaryPackConstants; -import com.android.inputmethod.event.EventInterpreter; -import com.android.inputmethod.keyboard.KeyDetector; +import com.android.inputmethod.event.Event; +import com.android.inputmethod.event.HardwareEventDecoder; +import com.android.inputmethod.event.HardwareKeyboardEventDecoder; +import com.android.inputmethod.event.InputTransaction; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardId; @@ -77,232 +71,174 @@ import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.ProductionFlag; +import com.android.inputmethod.latin.inputlogic.InputLogic; import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever; -import com.android.inputmethod.latin.personalization.PersonalizationDictionary; -import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister; +import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegistrar; import com.android.inputmethod.latin.personalization.PersonalizationHelper; -import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary; -import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.settings.Settings; 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.utils.ApplicationUtils; -import com.android.inputmethod.latin.utils.AsyncResultHolder; -import com.android.inputmethod.latin.utils.AutoCorrectionUtils; import com.android.inputmethod.latin.utils.CapsModeUtils; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.CompletionInfoUtils; -import com.android.inputmethod.latin.utils.InputTypeUtils; +import com.android.inputmethod.latin.utils.CoordinateUtils; +import com.android.inputmethod.latin.utils.DialogUtils; +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.LatinImeLoggerUtils; -import com.android.inputmethod.latin.utils.RecapitalizeStatus; -import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper; -import com.android.inputmethod.latin.utils.StringUtils; -import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask; -import com.android.inputmethod.latin.utils.TextRange; -import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils; +import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; +import com.android.inputmethod.latin.utils.StatsUtils; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import com.android.inputmethod.research.ResearchLogger; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Locale; -import java.util.TreeSet; +import java.util.concurrent.TimeUnit; /** * Input method implementation for Qwerty'ish keyboard. */ public class LatinIME extends InputMethodService implements KeyboardActionListener, - SuggestionStripView.Listener, TargetPackageInfoGetterTask.OnTargetPackageInfoKnownListener, - Suggest.SuggestInitializationListener { + SuggestionStripView.Listener, SuggestionStripViewAccessor, + DictionaryFacilitatorForSuggest.DictionaryInitializationListener, + ImportantNoticeDialog.ImportantNoticeDialogListener { private static final String TAG = LatinIME.class.getSimpleName(); private static final boolean TRACE = false; - private static boolean DEBUG; + private static boolean DEBUG = false; private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100; - // How many continuous deletes at which to start deleting at a higher speed. - private static final int DELETE_ACCELERATE_AT = 20; - // Key events coming any faster than this are long-presses. - private static final int QUICK_PRESS = 200; - private static final int PENDING_IMS_CALLBACK_DURATION = 800; private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2; - // TODO: Set this value appropriately. - private static final int GET_SUGGESTED_WORDS_TIMEOUT = 200; - /** * The name of the scheme used by the Package Manager to warn of a new package installation, * replacement or removal. */ private static final String SCHEME_PACKAGE = "package"; - private static final int SPACE_STATE_NONE = 0; - // Double space: the state where the user pressed space twice quickly, which LatinIME - // resolved as period-space. Undoing this converts the period to a space. - private static final int SPACE_STATE_DOUBLE = 1; - // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip - // have just been swapped. Undoing this swaps them back; the space is still considered weak. - private static final int SPACE_STATE_SWAP_PUNCTUATION = 2; - // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak - // spaces happen when the user presses space, accepting the current suggestion (whether - // it's an auto-correction or not). - private static final int SPACE_STATE_WEAK = 3; - // Phantom space: a not-yet-inserted space that should get inserted on the next input, - // character provided it's not a separator. If it's a separator, the phantom space is dropped. - // Phantom spaces happen when a user chooses a word from the suggestion strip. - private static final int SPACE_STATE_PHANTOM = 4; - - // Current space state of the input method. This can be any of the above constants. - private int mSpaceState; - private final Settings mSettings; + private final InputLogic mInputLogic = new InputLogic(this /* LatinIME */, + this /* SuggestionStripViewAccessor */); + // We expect to have only one decoder in almost all cases, hence the default capacity of 1. + // If it turns out we need several, it will get grown seamlessly. + final SparseArray<HardwareEventDecoder> mHardwareEventDecoders + = new SparseArray<HardwareEventDecoder>(1); private View mExtractArea; private View mKeyPreviewBackingView; private SuggestionStripView mSuggestionStripView; - // Never null - private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; - private Suggest mSuggest; - private CompletionInfo[] mApplicationSpecifiedCompletions; - private AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils(); private RichInputMethodManager mRichImm; @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher; private final SubtypeSwitcher mSubtypeSwitcher; private final SubtypeState mSubtypeState = new SubtypeState(); - // At start, create a default event interpreter that does nothing by passing it no decoder spec. - // The event interpreter should never be null. - private EventInterpreter mEventInterpreter = new EventInterpreter(this); - - private boolean mIsMainDictionaryAvailable; - private UserBinaryDictionary mUserDictionary; - private UserHistoryDictionary mUserHistoryDictionary; - private PersonalizationPredictionDictionary mPersonalizationPredictionDictionary; - private PersonalizationDictionary mPersonalizationDictionary; - private boolean mIsUserDictionaryAvailable; - - private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - private final WordComposer mWordComposer = new WordComposer(); - private final RichInputConnection mConnection = new RichInputConnection(this); - private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); - - // Keep track of the last selection range to decide if we need to show word alternatives - private static final int NOT_A_CURSOR_POSITION = -1; - private int mLastSelectionStart = NOT_A_CURSOR_POSITION; - private int mLastSelectionEnd = NOT_A_CURSOR_POSITION; - - // Whether we are expecting an onUpdateSelection event to fire. If it does when we don't - // "expect" it, it means the user actually moved the cursor. - private boolean mExpectingUpdateSelection; - private int mDeleteCount; - private long mLastKeyTime; - private final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet(); - // Personalization debugging params - private boolean mUseOnlyPersonalizationDictionaryForDebug = false; - private boolean mBoostPersonalizationDictionaryForDebug = false; - - // Member variables for remembering the current device orientation. - private int mDisplayOrientation; // Object for reacting to adding/removing a dictionary pack. private BroadcastReceiver mDictionaryPackInstallReceiver = new DictionaryPackInstallBroadcastReceiver(this); - // Keeps track of most recently inserted text (multi-character key) for reverting - private String mEnteredText; - - // TODO: This boolean is persistent state and causes large side effects at unexpected times. - // Find a way to remove it for readability. - private boolean mIsAutoCorrectionIndicatorOn; + private BroadcastReceiver mDictionaryDumpBroadcastReceiver = + new DictionaryDumpBroadcastReceiver(this); private AlertDialog mOptionsDialog; private final boolean mIsHardwareAcceleratedDrawingEnabled; public final UIHandler mHandler = new UIHandler(this); - private InputUpdater mInputUpdater; - public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> { + public static final class UIHandler extends LeakGuardHandlerWrapper<LatinIME> { private static final int MSG_UPDATE_SHIFT_STATE = 0; private static final int MSG_PENDING_IMS_CALLBACK = 1; private static final int MSG_UPDATE_SUGGESTION_STRIP = 2; private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3; private static final int MSG_RESUME_SUGGESTIONS = 4; private static final int MSG_REOPEN_DICTIONARIES = 5; - private static final int MSG_ON_END_BATCH_INPUT = 6; + private static final int MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED = 6; private static final int MSG_RESET_CACHES = 7; + // Update this when adding new messages + private static final int MSG_LAST = MSG_RESET_CACHES; private static final int ARG1_NOT_GESTURE_INPUT = 0; private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1; private static final int ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT = 2; - private static final int ARG2_WITHOUT_TYPED_WORD = 0; - private static final int ARG2_WITH_TYPED_WORD = 1; + private static final int ARG2_UNUSED = 0; private int mDelayUpdateSuggestions; private int mDelayUpdateShiftState; - private long mDoubleSpacePeriodTimeout; - private long mDoubleSpacePeriodTimerStart; - public UIHandler(final LatinIME outerInstance) { - super(outerInstance); + public UIHandler(final LatinIME ownerInstance) { + super(ownerInstance); } public void onCreate() { - final Resources res = getOuterInstance().getResources(); - mDelayUpdateSuggestions = - res.getInteger(R.integer.config_delay_update_suggestions); - mDelayUpdateShiftState = - res.getInteger(R.integer.config_delay_update_shift_state); - mDoubleSpacePeriodTimeout = - res.getInteger(R.integer.config_double_space_period_timeout); + final LatinIME latinIme = getOwnerInstance(); + if (latinIme == null) { + return; + } + final Resources res = latinIme.getResources(); + mDelayUpdateSuggestions = res.getInteger(R.integer.config_delay_update_suggestions); + mDelayUpdateShiftState = res.getInteger(R.integer.config_delay_update_shift_state); } @Override public void handleMessage(final Message msg) { - final LatinIME latinIme = getOuterInstance(); + final LatinIME latinIme = getOwnerInstance(); + if (latinIme == null) { + return; + } final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher; switch (msg.what) { case MSG_UPDATE_SUGGESTION_STRIP: - latinIme.updateSuggestionStrip(); + cancelUpdateSuggestionStrip(); + latinIme.mInputLogic.performUpdateSuggestionStripSync( + latinIme.mSettings.getCurrent()); break; case MSG_UPDATE_SHIFT_STATE: - switcher.updateShiftState(); + switcher.requestUpdatingShiftState(latinIme.getCurrentAutoCapsState(), + latinIme.getCurrentRecapitalizeState()); break; case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: if (msg.arg1 == ARG1_NOT_GESTURE_INPUT) { - if (msg.arg2 == ARG2_WITH_TYPED_WORD) { - final Pair<SuggestedWords, String> p = - (Pair<SuggestedWords, String>) msg.obj; - latinIme.showSuggestionStripWithTypedWord(p.first, p.second); - } else { - latinIme.showSuggestionStrip((SuggestedWords) msg.obj); - } + final SuggestedWords suggestedWords = (SuggestedWords) msg.obj; + latinIme.showSuggestionStrip(suggestedWords); } else { latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj, msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT); } break; case MSG_RESUME_SUGGESTIONS: - latinIme.restartSuggestionsOnWordTouchedByCursor(); + latinIme.mInputLogic.restartSuggestionsOnWordTouchedByCursor( + latinIme.mSettings.getCurrent(), + false /* includeResumedWordInSuggestions */); break; case MSG_REOPEN_DICTIONARIES: - latinIme.initSuggest(); + latinIme.resetSuggest(); // In theory we could call latinIme.updateSuggestionStrip() right away, but // in the practice, the dictionary is not finished opening yet so we wouldn't // get any suggestions. Wait one frame. postUpdateSuggestionStrip(); break; - case MSG_ON_END_BATCH_INPUT: - latinIme.onEndBatchInputAsyncInternal((SuggestedWords) msg.obj); + case MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED: + latinIme.mInputLogic.onUpdateTailBatchInputCompleted( + latinIme.mSettings.getCurrent(), + (SuggestedWords) msg.obj, latinIme.mKeyboardSwitcher); break; case MSG_RESET_CACHES: - latinIme.retryResetCaches(msg.arg1 == 1 /* tryResumeSuggestions */, - msg.arg2 /* remainingTries */); + final SettingsValues settingsValues = latinIme.mSettings.getCurrent(); + if (latinIme.mInputLogic.retryResetCachesAndReturnSuccess(settingsValues, + msg.arg1 == 1 /* tryResumeSuggestions */, + msg.arg2 /* remainingTries */, this /* handler */)) { + // If we were able to reset the caches, then we can reload the keyboard. + // Otherwise, we'll do it when we can. + latinIme.mKeyboardSwitcher.loadKeyboard(latinIme.getCurrentInputEditorInfo(), + settingsValues, latinIme.getCurrentAutoCapsState(), + latinIme.getCurrentRecapitalizeState()); + } break; } } @@ -316,6 +252,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } public void postResumeSuggestions() { + final LatinIME latinIme = getOwnerInstance(); + if (latinIme == null) { + return; + } + if (!latinIme.mSettings.getCurrent().isSuggestionStripVisible()) { + return; + } removeMessages(MSG_RESUME_SUGGESTIONS); sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions); } @@ -343,8 +286,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState); } - public void cancelUpdateShiftState() { - removeMessages(MSG_UPDATE_SHIFT_STATE); + @UsedForTesting + public void removeAllMessages() { + for (int i = 0; i <= MSG_LAST; ++i) { + removeMessages(i); + } } public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, @@ -354,39 +300,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT; obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, - ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget(); + ARG2_UNUSED, suggestedWords).sendToTarget(); } public void showSuggestionStrip(final SuggestedWords suggestedWords) { removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, - ARG1_NOT_GESTURE_INPUT, ARG2_WITHOUT_TYPED_WORD, suggestedWords).sendToTarget(); + ARG1_NOT_GESTURE_INPUT, ARG2_UNUSED, suggestedWords).sendToTarget(); } - // TODO: Remove this method. - public void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, - final String typedWord) { - removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); - obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, ARG1_NOT_GESTURE_INPUT, - ARG2_WITH_TYPED_WORD, - new Pair<SuggestedWords, String>(suggestedWords, typedWord)).sendToTarget(); - } - - public void onEndBatchInput(final SuggestedWords suggestedWords) { - obtainMessage(MSG_ON_END_BATCH_INPUT, suggestedWords).sendToTarget(); - } - - public void startDoubleSpacePeriodTimer() { - mDoubleSpacePeriodTimerStart = SystemClock.uptimeMillis(); - } - - public void cancelDoubleSpacePeriodTimer() { - mDoubleSpacePeriodTimerStart = 0; - } - - public boolean isAcceptingDoubleSpacePeriod() { - return SystemClock.uptimeMillis() - mDoubleSpacePeriodTimerStart - < mDoubleSpacePeriodTimeout; + public void showTailBatchInputResult(final SuggestedWords suggestedWords) { + obtainMessage(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED, suggestedWords).sendToTarget(); } // Working variables for the following methods. @@ -401,7 +325,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen removeMessages(MSG_PENDING_IMS_CALLBACK); resetPendingImsCallback(); mIsOrientationChanging = true; - final LatinIME latinIme = getOuterInstance(); + final LatinIME latinIme = getOwnerInstance(); + if (latinIme == null) { + return; + } if (latinIme.isInputViewShown()) { latinIme.mKeyboardSwitcher.saveKeyboardState(); } @@ -415,12 +342,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo, boolean restarting) { - if (mHasPendingFinishInputView) + if (mHasPendingFinishInputView) { latinIme.onFinishInputViewInternal(mHasPendingFinishInput); - if (mHasPendingFinishInput) + } + if (mHasPendingFinishInput) { latinIme.onFinishInputInternal(); - if (mHasPendingStartInput) + } + if (mHasPendingStartInput) { latinIme.onStartInputInternal(editorInfo, restarting); + } resetPendingImsCallback(); } @@ -434,9 +364,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mIsOrientationChanging = false; mPendingSuccessiveImsCallback = true; } - final LatinIME latinIme = getOuterInstance(); - executePendingImsCallback(latinIme, editorInfo, restarting); - latinIme.onStartInputInternal(editorInfo, restarting); + final LatinIME latinIme = getOwnerInstance(); + if (latinIme != null) { + executePendingImsCallback(latinIme, editorInfo, restarting); + latinIme.onStartInputInternal(editorInfo, restarting); + if (ProductionFlag.USES_CURSOR_ANCHOR_MONITOR) { + // Currently we need to call this every time when the IME is attached to + // new application. + // TODO: Consider if we can do this automatically in the framework. + InputMethodServiceCompatUtils.setCursorAnchorMonitorMode(latinIme, 1); + } + } } } @@ -453,10 +391,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen sendMessageDelayed(obtainMessage(MSG_PENDING_IMS_CALLBACK), PENDING_IMS_CALLBACK_DURATION); } - final LatinIME latinIme = getOuterInstance(); - executePendingImsCallback(latinIme, editorInfo, restarting); - latinIme.onStartInputViewInternal(editorInfo, restarting); - mAppliedEditorInfo = editorInfo; + final LatinIME latinIme = getOwnerInstance(); + if (latinIme != null) { + executePendingImsCallback(latinIme, editorInfo, restarting); + latinIme.onStartInputViewInternal(editorInfo, restarting); + mAppliedEditorInfo = editorInfo; + } } } @@ -465,9 +405,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Typically this is the first onFinishInputView after orientation changed. mHasPendingFinishInputView = true; } else { - final LatinIME latinIme = getOuterInstance(); - latinIme.onFinishInputViewInternal(finishingInput); - mAppliedEditorInfo = null; + final LatinIME latinIme = getOwnerInstance(); + if (latinIme != null) { + latinIme.onFinishInputViewInternal(finishingInput); + mAppliedEditorInfo = null; + } } } @@ -476,9 +418,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Typically this is the first onFinishInput after orientation changed. mHasPendingFinishInput = true; } else { - final LatinIME latinIme = getOuterInstance(); - executePendingImsCallback(latinIme, null, false); - latinIme.onFinishInputInternal(); + final LatinIME latinIme = getOwnerInstance(); + if (latinIme != null) { + executePendingImsCallback(latinIme, null, false); + latinIme.onFinishInputInternal(); + } } } } @@ -536,7 +480,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen KeyboardSwitcher.init(this); AudioAndHapticFeedbackManager.init(this); AccessibilityUtils.init(this); - PersonalizationDictionarySessionRegister.init(this); super.onCreate(); @@ -545,19 +488,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: Resolve mutual dependencies of {@link #loadSettings()} and {@link #initSuggest()}. loadSettings(); - initSuggest(); + resetSuggest(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest); + ResearchLogger.getInstance().init(this, mKeyboardSwitcher); + ResearchLogger.getInstance().initDictionary( + mInputLogic.mSuggest.mDictionaryFacilitator); } - mDisplayOrientation = getResources().getConfiguration().orientation; // Register to receive ringer mode change and network state change. // Also receive installation and removal of a dictionary pack. final IntentFilter filter = new IntentFilter(); filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION); - registerReceiver(mReceiver, filter); + registerReceiver(mConnectivityAndRingerModeChangeReceiver, filter); final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); @@ -569,47 +513,74 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); registerReceiver(mDictionaryPackInstallReceiver, newDictFilter); + final IntentFilter dictDumpFilter = new IntentFilter(); + dictDumpFilter.addAction(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION); + registerReceiver(mDictionaryDumpBroadcastReceiver, dictDumpFilter); + DictionaryDecayBroadcastReciever.setUpIntervalAlarmForDictionaryDecaying(this); - mInputUpdater = new InputUpdater(this); + StatsUtils.onCreateCompleted(this); } // Has to be package-visible for unit tests @UsedForTesting void loadSettings() { final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); - final InputAttributes inputAttributes = - new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode()); - mSettings.loadSettings(locale, inputAttributes); - AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent()); - // To load the keyboard we need to load all the settings once, but resetting the - // contacts dictionary should be deferred until after the new layout has been displayed - // to improve responsivity. In the language switching process, we post a reopenDictionaries - // message, then come here to read the settings for the new language before we change - // the layout; at this time, we need to skip resetting the contacts dictionary. It will - // be done later inside {@see #initSuggest()} when the reopenDictionaries message is - // processed. + final EditorInfo editorInfo = getCurrentInputEditorInfo(); + final InputAttributes inputAttributes = new InputAttributes(editorInfo, isFullscreenMode()); + mSettings.loadSettings(this, locale, 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()) { - // May need to reset the contacts dictionary depending on the user settings. - resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary()); + resetSuggestForLocale(locale); + } + refreshPersonalizationDictionarySession(); + } + + private void refreshPersonalizationDictionarySession() { + final DictionaryFacilitatorForSuggest dictionaryFacilitator = + mInputLogic.mSuggest.mDictionaryFacilitator; + final boolean shouldKeepUserHistoryDictionaries; + final boolean shouldKeepPersonalizationDictionaries; + if (mSettings.getCurrent().mUsePersonalizedDicts) { + shouldKeepUserHistoryDictionaries = true; + // TODO: Eliminate this restriction + shouldKeepPersonalizationDictionaries = + mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes(); + } else { + shouldKeepUserHistoryDictionaries = false; + shouldKeepPersonalizationDictionaries = false; + } + if (!shouldKeepUserHistoryDictionaries) { + // Remove user history dictionaries. + PersonalizationHelper.removeAllUserHistoryDictionaries(this); + dictionaryFacilitator.clearUserHistoryDictionary(); + } + if (!shouldKeepPersonalizationDictionaries) { + // Remove personalization dictionaries. + PersonalizationHelper.removeAllPersonalizationDictionaries(this); + PersonalizationDictionarySessionRegistrar.resetAll(this); + } else { + PersonalizationDictionarySessionRegistrar.init(this, dictionaryFacilitator); } } // Note that this method is called from a non-UI thread. @Override public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) { - mIsMainDictionaryAvailable = isMainDictionaryAvailable; final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); if (mainKeyboardView != null) { mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable); } } - private void initSuggest() { + private void resetSuggest() { final Locale switcherSubtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); final String switcherLocaleStr = switcherSubtypeLocale.toString(); final Locale subtypeLocale; - final String localeStr; if (TextUtils.isEmpty(switcherLocaleStr)) { // 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 @@ -619,132 +590,80 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // of knowing anyway. Log.e(TAG, "System is reporting no current subtype."); subtypeLocale = getResources().getConfiguration().locale; - localeStr = subtypeLocale.toString(); } else { subtypeLocale = switcherSubtypeLocale; - localeStr = switcherLocaleStr; - } - - final Suggest newSuggest = new Suggest(this /* Context */, subtypeLocale, - this /* SuggestInitializationListener */); - final SettingsValues settingsValues = mSettings.getCurrent(); - if (settingsValues.mCorrectionEnabled) { - newSuggest.setAutoCorrectionThreshold(settingsValues.mAutoCorrectionThreshold); } - - mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().initSuggest(newSuggest); - } - - mUserDictionary = new UserBinaryDictionary(this, localeStr); - mIsUserDictionaryAvailable = mUserDictionary.isEnabled(); - newSuggest.setUserDictionary(mUserDictionary); - - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - - mUserHistoryDictionary = PersonalizationHelper.getUserHistoryDictionary( - this, localeStr, prefs); - newSuggest.setUserHistoryDictionary(mUserHistoryDictionary); - mPersonalizationDictionary = PersonalizationHelper - .getPersonalizationDictionary(this, localeStr, prefs); - newSuggest.setPersonalizationDictionary(mPersonalizationDictionary); - mPersonalizationPredictionDictionary = PersonalizationHelper - .getPersonalizationPredictionDictionary(this, localeStr, prefs); - newSuggest.setPersonalizationPredictionDictionary(mPersonalizationPredictionDictionary); - - final Suggest oldSuggest = mSuggest; - resetContactsDictionary(null != oldSuggest ? oldSuggest.getContactsDictionary() : null); - mSuggest = newSuggest; - if (oldSuggest != null) oldSuggest.close(); + resetSuggestForLocale(subtypeLocale); } /** - * Resets the contacts dictionary in mSuggest according to the user settings. - * - * This method takes an optional contacts dictionary to use when the locale hasn't changed - * since the contacts dictionary can be opened or closed as necessary depending on the settings. + * Reset suggest by loading dictionaries for the locale and the current settings values. * - * @param oldContactsDictionary an optional dictionary to use, or null + * @param locale the locale */ - private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) { - final Suggest suggest = mSuggest; - final boolean shouldSetDictionary = - (null != suggest && mSettings.getCurrent().mUseContactsDict); - - final ContactsBinaryDictionary dictionaryToUse; - if (!shouldSetDictionary) { - // Make sure the dictionary is closed. If it is already closed, this is a no-op, - // so it's safe to call it anyways. - if (null != oldContactsDictionary) oldContactsDictionary.close(); - dictionaryToUse = null; - } else { - final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale(); - if (null != oldContactsDictionary) { - if (!oldContactsDictionary.mLocale.equals(locale)) { - // If the locale has changed then recreate the contacts dictionary. This - // allows locale dependent rules for handling bigram name predictions. - oldContactsDictionary.close(); - dictionaryToUse = new ContactsBinaryDictionary(this, locale); - } else { - // Make sure the old contacts dictionary is opened. If it is already open, - // this is a no-op, so it's safe to call it anyways. - oldContactsDictionary.reopen(this); - dictionaryToUse = oldContactsDictionary; - } - } else { - dictionaryToUse = new ContactsBinaryDictionary(this, locale); - } - } - - if (null != suggest) { - suggest.setContactsDictionary(dictionaryToUse); + private void resetSuggestForLocale(final Locale locale) { + final DictionaryFacilitatorForSuggest dictionaryFacilitator = + mInputLogic.mSuggest.mDictionaryFacilitator; + final SettingsValues settingsValues = mSettings.getCurrent(); + dictionaryFacilitator.resetDictionaries(this /* context */, locale, + settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts, + false /* forceReloadMainDictionary */, this); + if (settingsValues.mCorrectionEnabled) { + mInputLogic.mSuggest.setAutoCorrectionThreshold( + settingsValues.mAutoCorrectionThreshold); } } + /** + * Reset suggest by loading the main dictionary of the current locale. + */ /* package private */ void resetSuggestMainDict() { - final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); - mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */); - mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale); + final DictionaryFacilitatorForSuggest dictionaryFacilitator = + mInputLogic.mSuggest.mDictionaryFacilitator; + final SettingsValues settingsValues = mSettings.getCurrent(); + dictionaryFacilitator.resetDictionaries(this /* context */, + dictionaryFacilitator.getLocale(), settingsValues.mUseContactsDict, + settingsValues.mUsePersonalizedDicts, true /* forceReloadMainDictionary */, this); } @Override public void onDestroy() { - final Suggest suggest = mSuggest; - if (suggest != null) { - suggest.close(); - mSuggest = null; - } + mInputLogic.mSuggest.mDictionaryFacilitator.closeDictionaries(); mSettings.onDestroy(); - unregisterReceiver(mReceiver); + unregisterReceiver(mConnectivityAndRingerModeChangeReceiver); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.getInstance().onDestroy(); } unregisterReceiver(mDictionaryPackInstallReceiver); - PersonalizationDictionarySessionRegister.onDestroy(this); + unregisterReceiver(mDictionaryDumpBroadcastReceiver); + PersonalizationDictionarySessionRegistrar.close(this); LatinImeLogger.commit(); LatinImeLogger.onDestroy(); - if (mInputUpdater != null) { - mInputUpdater.quitLooper(); - } + StatsUtils.onDestroy(); super.onDestroy(); } + @UsedForTesting + public void recycle() { + unregisterReceiver(mDictionaryPackInstallReceiver); + unregisterReceiver(mDictionaryDumpBroadcastReceiver); + unregisterReceiver(mConnectivityAndRingerModeChangeReceiver); + mInputLogic.recycle(); + } + @Override public void onConfigurationChanged(final Configuration conf) { // If orientation changed while predicting, commit the change - if (mDisplayOrientation != conf.orientation) { - mDisplayOrientation = conf.orientation; + final SettingsValues settingsValues = mSettings.getCurrent(); + if (settingsValues.mDisplayOrientation != conf.orientation) { mHandler.startOrientationChanging(); - mConnection.beginBatchEdit(); - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - mConnection.finishComposingText(); - mConnection.endBatchEdit(); - if (isShowingOptionDialog()) { - mOptionsDialog.dismiss(); - } + mInputLogic.mConnection.beginBatchEdit(); + mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR); + mInputLogic.mConnection.finishComposingText(); + mInputLogic.mConnection.endBatchEdit(); } - PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf); + PersonalizationDictionarySessionRegistrar.onConfigurationChanged(this, conf, + mInputLogic.mSuggest.mDictionaryFacilitator); super.onConfigurationChanged(conf); } @@ -760,8 +679,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen .findViewById(android.R.id.extractArea); mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing); mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view); - if (mSuggestionStripView != null) + if (hasSuggestionStripView()) { mSuggestionStripView.setListener(this, view); + } if (LatinImeLogger.sVISUALDEBUG) { mKeyPreviewBackingView.setBackgroundColor(0x10FF0000); } @@ -834,29 +754,21 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen + ", word caps = " + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0)); } + Log.i(TAG, "Starting input. Cursor position = " + + editorInfo.initialSelStart + "," + editorInfo.initialSelEnd); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, prefs); } if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) { - Log.w(TAG, "Deprecated private IME option specified: " - + editorInfo.privateImeOptions); + Log.w(TAG, "Deprecated private IME option specified: " + editorInfo.privateImeOptions); Log.w(TAG, "Use " + getPackageName() + "." + NO_MICROPHONE + " instead"); } if (InputAttributes.inPrivateImeOptions(getPackageName(), FORCE_ASCII, editorInfo)) { - Log.w(TAG, "Deprecated private IME option specified: " - + editorInfo.privateImeOptions); + Log.w(TAG, "Deprecated private IME option specified: " + editorInfo.privateImeOptions); Log.w(TAG, "Use EditorInfo.IME_FLAG_FORCE_ASCII flag instead"); } - final PackageInfo packageInfo = - TargetPackageInfoGetterTask.getCachedPackageInfo(editorInfo.packageName); - mAppWorkAroundsUtils.setPackageInfo(packageInfo); - if (null == packageInfo) { - new TargetPackageInfoGetterTask(this /* context */, this /* listener */) - .execute(editorInfo.packageName); - } - LatinImeLogger.onStartInputView(editorInfo); // In landscape mode, this method gets called without the input view being created. if (mainKeyboardView == null) { @@ -878,57 +790,53 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // The EditorInfo might have a flag that affects fullscreen mode. // Note: This call should be done by InputMethodService? updateFullscreenMode(); - mApplicationSpecifiedCompletions = null; // The app calling setText() has the effect of clearing the composing // span, so we should reset our state unconditionally, even if restarting is true. - mEnteredText = null; - resetComposingState(true /* alsoResetLastComposedWord */); - mDeleteCount = 0; - mSpaceState = SPACE_STATE_NONE; - mRecapitalizeStatus.deactivate(); - mCurrentlyPressedHardwareKeys.clear(); + mInputLogic.startInput(restarting, editorInfo); // Note: the following does a round-trip IPC on the main thread: be careful final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale(); - final Suggest suggest = mSuggest; - if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) { - initSuggest(); + final Suggest suggest = mInputLogic.mSuggest; + if (null != currentLocale && !currentLocale.equals(suggest.getLocale())) { + // TODO: Do this automatically. + resetSuggest(); } - if (mSuggestionStripView != null) { - // This will set the punctuation suggestions if next word suggestion is off; - // otherwise it will clear the suggestion strip. - setPunctuationSuggestions(); - } - mSuggestedWords = SuggestedWords.EMPTY; - // Sometimes, while rotating, for some reason the framework tells the app we are not - // connected to it and that means we can't refresh the cache. In this case, schedule a - // refresh later. + // TODO[IL]: Can the following be moved to InputLogic#startInput? final boolean canReachInputConnection; - if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(editorInfo.initialSelStart, + if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess( + editorInfo.initialSelStart, editorInfo.initialSelEnd, false /* shouldFinishComposition */)) { + // Sometimes, while rotating, for some reason the framework tells the app we are not + // connected to it and that means we can't refresh the cache. In this case, schedule a + // refresh later. // We try resetting the caches up to 5 times before giving up. mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */); // mLastSelection{Start,End} are reset later in this method, don't need to do it here canReachInputConnection = false; } else { - if (isDifferentTextField) { - mHandler.postResumeSuggestions(); - } + // When rotating, initialSelStart and initialSelEnd sometimes are lying. Make a best + // effort to work around this bug. + mInputLogic.mConnection.tryFixLyingCursorPosition(); + mHandler.postResumeSuggestions(); canReachInputConnection = true; } + if (isDifferentTextField || + !currentSettingsValues.hasSameOrientation(getResources().getConfiguration())) { + loadSettings(); + } if (isDifferentTextField) { mainKeyboardView.closing(); - loadSettings(); currentSettingsValues = mSettings.getCurrent(); - if (suggest != null && currentSettingsValues.mCorrectionEnabled) { + if (currentSettingsValues.mCorrectionEnabled) { suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold); } - switcher.loadKeyboard(editorInfo, currentSettingsValues); + switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); if (!canReachInputConnection) { // If we can't reach the input connection, we will call loadKeyboard again later, // so we need to save its state now. The call will be done in #retryResetCaches. @@ -937,26 +845,23 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } else if (restarting) { // TODO: Come up with a more comprehensive way to reset the keyboard layout when // a keyboard layout set doesn't get reloaded in this method. - switcher.resetKeyboardStateToAlphabet(); + switcher.resetKeyboardStateToAlphabet(getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); // In apps like Talk, we come here when the text is sent and the field gets emptied and // we need to re-evaluate the shift state, but not the whole layout which would be // disruptive. // Space state must be updated before calling updateShiftState - switcher.updateShiftState(); + switcher.requestUpdatingShiftState(getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); } - setSuggestionStripShownInternal( - isSuggestionsStripVisible(), /* needsInputViewShown */ false); - - mLastSelectionStart = editorInfo.initialSelStart; - mLastSelectionEnd = editorInfo.initialSelEnd; - // 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. - tryFixLyingCursorPosition(); + // This will set the punctuation suggestions if next word suggestion is off; + // otherwise it will clear the suggestion strip. + setNeutralSuggestionStrip(); mHandler.cancelUpdateSuggestionStrip(); - mHandler.cancelDoubleSpacePeriodTimer(); - mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable); + mainKeyboardView.setMainDictionaryAvailability( + suggest.mDictionaryFacilitator.hasInitializedMainDictionary()); mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn, currentSettingsValues.mKeyPreviewPopupDismissDelay); mainKeyboardView.setSlidingKeyInputPreviewEnabled( @@ -966,76 +871,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen currentSettingsValues.mGestureTrailEnabled, currentSettingsValues.mGestureFloatingPreviewTextEnabled); - initPersonalizationDebugSettings(currentSettingsValues); - if (TRACE) Debug.startMethodTracing("/data/trace/latinime"); } - /** - * 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 - * 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. - */ - private void tryFixLyingCursorPosition() { - final CharSequence textBeforeCursor = - mConnection.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); - if (null == textBeforeCursor) { - mLastSelectionStart = mLastSelectionEnd = NOT_A_CURSOR_POSITION; - } else { - final int textLength = textBeforeCursor.length(); - if (textLength > mLastSelectionStart - || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE - && mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { - // It should not be possible to have only one of those variables be - // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized - // (simple cursor, no selection) or there is no cursor/we don't know its pos - final boolean wasEqual = mLastSelectionStart == mLastSelectionEnd; - mLastSelectionStart = textLength; - // We can't figure out the value of mLastSelectionEnd :( - // But at least if it's smaller than mLastSelectionStart something is wrong, - // and if they used to be equal we also don't want to make it look like there is a - // selection. - if (wasEqual || mLastSelectionStart > mLastSelectionEnd) { - mLastSelectionEnd = mLastSelectionStart; - } - } - } - } - - // Initialization of personalization debug settings. This must be called inside - // onStartInputView. - private void initPersonalizationDebugSettings(SettingsValues currentSettingsValues) { - if (mUseOnlyPersonalizationDictionaryForDebug - != currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug) { - // Only for debug - initSuggest(); - mUseOnlyPersonalizationDictionaryForDebug = - currentSettingsValues.mUseOnlyPersonalizationDictionaryForDebug; - } - - if (mBoostPersonalizationDictionaryForDebug != - currentSettingsValues.mBoostPersonalizationDictionaryForDebug) { - // Only for debug - mBoostPersonalizationDictionaryForDebug = - currentSettingsValues.mBoostPersonalizationDictionaryForDebug; - if (mBoostPersonalizationDictionaryForDebug) { - UserHistoryForgettingCurveUtils.boostMaxFreqForDebug(); - } else { - UserHistoryForgettingCurveUtils.resetMaxFreqForDebug(); - } - } - } - - // Callback for the TargetPackageInfoGetterTask - @Override - public void onTargetPackageInfoKnown(final PackageInfo info) { - mAppWorkAroundsUtils.setPackageInfo(info); - } - @Override public void onWindowHidden() { super.onWindowHidden(); @@ -1062,12 +900,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestionStrip(); // Should do the following in onFinishInputInternal but until JB MR2 it's not called :( - if (mWordComposer.isComposingWord()) mConnection.finishComposingText(); - resetComposingState(true /* alsoResetLastComposedWord */); + mInputLogic.finishInput(); // Notify ResearchLogger if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart, - mLastSelectionEnd, getCurrentInputConnection()); + ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput); } } @@ -1078,104 +914,38 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, composingSpanEnd); if (DEBUG) { - Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart - + ", ose=" + oldSelEnd - + ", lss=" + mLastSelectionStart - + ", lse=" + mLastSelectionEnd - + ", nss=" + newSelStart - + ", nse=" + newSelEnd - + ", cs=" + composingSpanStart - + ", ce=" + composingSpanEnd); + Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart + ", ose=" + oldSelEnd + + ", nss=" + newSelStart + ", nse=" + newSelEnd + + ", cs=" + composingSpanStart + ", ce=" + composingSpanEnd); } if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - final boolean expectingUpdateSelectionFromLogger = - ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection(); - ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd, + ResearchLogger.latinIME_onUpdateSelection(oldSelStart, oldSelEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart, - composingSpanEnd, mExpectingUpdateSelection, - expectingUpdateSelectionFromLogger, mConnection); - if (expectingUpdateSelectionFromLogger) { - // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work - return; - } + composingSpanEnd, mInputLogic.mConnection); } - final boolean selectionChanged = mLastSelectionStart != newSelStart - || mLastSelectionEnd != newSelEnd; - - // if composingSpanStart and composingSpanEnd are -1, it means there is no composing - // span in the view - we can use that to narrow down whether the cursor was moved - // by us or not. If we are composing a word but there is no composing span, then - // we know for sure the cursor moved while we were composing and we should reset - // the state. TODO: rescind this policy: the framework never removes the composing - // span on its own accord while editing. This test is useless. - final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1; - // If the keyboard is not visible, we don't need to do all the housekeeping work, as it // will be reset when the keyboard shows up anyway. // TODO: revisit this when LatinIME supports hardware keyboards. // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown(). // TODO: find a better way to simulate actual execution. - if (isInputViewShown() && !mExpectingUpdateSelection - && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) { - // TAKE CARE: there is a race condition when we enter this test even when the user - // did not explicitly move the cursor. This happens when typing fast, where two keys - // turn this flag on in succession and both onUpdateSelection() calls arrive after - // the second one - the first call successfully avoids this test, but the second one - // enters. For the moment we rely on noComposingSpan to further reduce the impact. - - // TODO: the following is probably better done in resetEntireInputState(). - // it should only happen when the cursor moved, and the very purpose of the - // test below is to narrow down whether this happened or not. Likewise with - // the call to updateShiftState. - // We set this to NONE because after a cursor move, we don't want the space - // state-related special processing to kick in. - mSpaceState = SPACE_STATE_NONE; - - // TODO: is it still necessary to test for composingSpan related stuff? - final boolean selectionChangedOrSafeToReset = selectionChanged - || (!mWordComposer.isComposingWord()) || noComposingSpan; - final boolean hasOrHadSelection = (oldSelStart != oldSelEnd - || newSelStart != newSelEnd); - final int moveAmount = newSelStart - oldSelStart; - if (selectionChangedOrSafeToReset && (hasOrHadSelection - || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { - // If we are composing a word and moving the cursor, we would want to set a - // suggestion span for recorrection to work correctly. Unfortunately, that - // would involve the keyboard committing some new text, which would move the - // cursor back to where it was. Latin IME could then fix the position of the cursor - // again, but the asynchronous nature of the calls results in this wreaking havoc - // with selection on double tap and the like. - // Another option would be to send suggestions each time we set the composing - // text, but that is probably too expensive to do, so we decided to leave things - // as is. - resetEntireInputState(newSelStart); - } else { - // resetEntireInputState calls resetCachesUponCursorMove, but with the second - // argument as true. But in all cases where we don't reset the entire input state, - // we still want to tell the rich input connection about the new cursor position so - // that it can update its caches. - mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, - false /* shouldFinishComposition */); - } - - // We moved the cursor. If we are touching a word, we need to resume suggestion, - // unless suggestions are off. - if (isSuggestionsStripVisible()) { - mHandler.postResumeSuggestions(); - } - // Reset the last recapitalization. - mRecapitalizeStatus.deactivate(); - mKeyboardSwitcher.updateShiftState(); + if (isInputViewShown() && + mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd)) { + mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); } - mExpectingUpdateSelection = false; - // Make a note of the cursor position - mLastSelectionStart = newSelStart; - mLastSelectionEnd = newSelEnd; mSubtypeState.currentSubtypeUsed(); } + @Override + public void onUpdateCursor(Rect rect) { + if (DEBUG) { + Log.i(TAG, "onUpdateCursor:" + rect.toShortString()); + } + super.onUpdateCursor(rect); + } + /** * This is called when the user has clicked on the extracted text view, * when running in fullscreen mode. The default implementation hides @@ -1186,7 +956,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen */ @Override public void onExtractedTextClicked() { - if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return; + if (mSettings.getCurrent().isSuggestionsRequested()) { + return; + } super.onExtractedTextClicked(); } @@ -1202,7 +974,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen */ @Override public void onExtractedCursorMovement(final int dx, final int dy) { - if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) return; + if (mSettings.getCurrent().isSuggestionsRequested()) { + return; + } super.onExtractedCursorMovement(dx, dy); } @@ -1217,7 +991,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } if (TRACE) Debug.stopMethodTracing(); - if (mOptionsDialog != null && mOptionsDialog.isShowing()) { + if (isShowingOptionDialog()) { mOptionsDialog.dismiss(); mOptionsDialog = null; } @@ -1234,58 +1008,30 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } } - if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) return; + if (!mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()) { + return; + } if (applicationSpecifiedCompletions == null) { - clearSuggestionStrip(); + setNeutralSuggestionStrip(); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_onDisplayCompletions(null); } return; } - mApplicationSpecifiedCompletions = - CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions); final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords = SuggestedWords.getFromApplicationSpecifiedCompletions( applicationSpecifiedCompletions); - final SuggestedWords suggestedWords = new SuggestedWords( - applicationSuggestedWords, - false /* typedWordValid */, - false /* hasAutoCorrectionCandidate */, - false /* isPunctuationSuggestions */, - false /* isObsoleteSuggestions */, - false /* isPrediction */); - // When in fullscreen mode, show completions generated by the application - final boolean isAutoCorrection = false; - setSuggestedWords(suggestedWords, isAutoCorrection); - setAutoCorrectionIndicator(isAutoCorrection); - setSuggestionStripShown(true); + final SuggestedWords suggestedWords = new SuggestedWords(applicationSuggestedWords, + null /* rawSuggestions */, false /* typedWordValid */, false /* willAutoCorrect */, + false /* isObsoleteSuggestions */, false /* isPrediction */); + // When in fullscreen mode, show completions generated by the application forcibly + setSuggestedWords(suggestedWords, true /* isSuggestionStripVisible */); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions); } } - private void setSuggestionStripShownInternal(final boolean shown, - final boolean needsInputViewShown) { - // TODO: Modify this if we support suggestions with hard keyboard - if (onEvaluateInputViewShown() && mSuggestionStripView != null) { - final boolean inputViewShown = mKeyboardSwitcher.isShowingMainKeyboardOrEmojiPalettes(); - final boolean shouldShowSuggestions = shown - && (needsInputViewShown ? inputViewShown : true); - if (isFullscreenMode()) { - mSuggestionStripView.setVisibility( - shouldShowSuggestions ? View.VISIBLE : View.GONE); - } else { - mSuggestionStripView.setVisibility( - shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE); - } - } - } - - private void setSuggestionStripShown(final boolean shown) { - setSuggestionStripShownInternal(shown, /* needsInputViewShown */true); - } - private int getAdjustedBackingViewHeight() { final int currentHeight = mKeyPreviewBackingView.getHeight(); if (currentHeight > 0) { @@ -1317,7 +1063,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen public void onComputeInsets(final InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView(); - if (visibleKeyboardView == null || mSuggestionStripView == null) { + if (visibleKeyboardView == null || !hasSuggestionStripView()) { return; } final int adjustedBackingHeight = getAdjustedBackingViewHeight(); @@ -1353,9 +1099,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public boolean onEvaluateFullscreenMode() { - // Reread resource value here, because this method is called by framework anytime as needed. - final boolean isFullscreenModeAllowed = - Settings.readUseFullscreenMode(getResources()); + // Reread resource value here, because this method is called by the framework as needed. + final boolean isFullscreenModeAllowed = Settings.readUseFullscreenMode(getResources()); if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) { // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI // implies NO_FULLSCREEN. However, the framework mistakenly does. i.e. NO_EXTRACT_UI @@ -1378,138 +1123,30 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE); } - // This will reset the whole input state to the starting state. It will clear - // the composing word, reset the last composed word, tell the inputconnection about it. - private void resetEntireInputState(final int newCursorPosition) { - final boolean shouldFinishComposition = mWordComposer.isComposingWord(); - resetComposingState(true /* alsoResetLastComposedWord */); - final SettingsValues settingsValues = mSettings.getCurrent(); - if (settingsValues.mBigramPredictionEnabled) { - clearSuggestionStrip(); - } else { - setSuggestedWords(settingsValues.mSuggestPuncList, false); - } - mConnection.resetCachesUponCursorMoveAndReturnSuccess(newCursorPosition, - shouldFinishComposition); + private int getCurrentAutoCapsState() { + return mInputLogic.getCurrentAutoCapsState(mSettings.getCurrent()); } - private void resetComposingState(final boolean alsoResetLastComposedWord) { - mWordComposer.reset(); - if (alsoResetLastComposedWord) - mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; + private int getCurrentRecapitalizeState() { + return mInputLogic.getCurrentRecapitalizeState(); } - private void commitTyped(final String separatorString) { - if (!mWordComposer.isComposingWord()) return; - final String typedWord = mWordComposer.getTypedWord(); - if (typedWord.length() > 0) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode()); - } - commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, - separatorString); - } + public Locale getCurrentSubtypeLocale() { + return mSubtypeSwitcher.getCurrentSubtypeLocale(); } - // Called from the KeyboardSwitcher which needs to know auto caps state to display - // the right layout. - public int getCurrentAutoCapsState() { - final SettingsValues currentSettingsValues = mSettings.getCurrent(); - if (!currentSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; - - final EditorInfo ei = getCurrentInputEditorInfo(); - if (ei == null) return Constants.TextUtils.CAP_MODE_OFF; - final int inputType = ei.inputType; - // Warning: this depends on mSpaceState, which may not be the most current value. If - // mSpaceState gets updated later, whoever called this may need to be told about it. - return mConnection.getCursorCapsMode(inputType, currentSettingsValues, - SPACE_STATE_PHANTOM == mSpaceState); - } - - public int getCurrentRecapitalizeState() { - if (!mRecapitalizeStatus.isActive() - || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { - // Not recapitalizing at the moment - return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; - } - return mRecapitalizeStatus.getCurrentMode(); - } - - // Factor in auto-caps and manual caps and compute the current caps mode. - private int getActualCapsMode() { - final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode(); - if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode; - final int auto = getCurrentAutoCapsState(); - if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { - return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; - } - if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED; - return WordComposer.CAPS_MODE_OFF; - } - - private void swapSwapperAndSpace() { - final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0); - // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. - if (lastTwo != null && lastTwo.length() == 2 - && lastTwo.charAt(0) == Constants.CODE_SPACE) { - mConnection.deleteSurroundingText(2, 0); - final String text = lastTwo.charAt(1) + " "; - mConnection.commitText(text, 1); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text); - } - mKeyboardSwitcher.updateShiftState(); - } - } - - private boolean maybeDoubleSpacePeriod() { - final SettingsValues currentSettingsValues = mSettings.getCurrent(); - if (!currentSettingsValues.mUseDoubleSpacePeriod) return false; - if (!mHandler.isAcceptingDoubleSpacePeriod()) return false; - // We only do this when we see two spaces and an accepted code point before the cursor. - // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars. - final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0); - if (null == lastThree) return false; - final int length = lastThree.length(); - if (length < 3) return false; - if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false; - if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false; - // We know there are spaces in pos -1 and -2, and we have at least three chars. - // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space, - // so this is fine. - final int firstCodePoint = - Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ? - Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3); - if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) { - mHandler.cancelDoubleSpacePeriodTimer(); - mConnection.deleteSurroundingText(2, 0); - final String textToInsert = new String( - new int[] { currentSettingsValues.mSentenceSeparator, Constants.CODE_SPACE }, - 0, 2); - mConnection.commitText(textToInsert, 1); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert, - false /* isBatchMode */); - } - mKeyboardSwitcher.updateShiftState(); - return true; + /** + * @param codePoints code points to get coordinates for. + * @return x,y coordinates for this keyboard, as a flattened array. + */ + public int[] getCoordinatesForCurrentKeyboard(final int[] codePoints) { + final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); + if (null == keyboard) { + return CoordinateUtils.newCoordinateArray(codePoints.length, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } else { + return keyboard.getCoordinates(codePoints); } - return false; - } - - private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) { - // TODO: Check again whether there really ain't a better way to check this. - // TODO: This should probably be language-dependant... - return Character.isLetterOrDigit(codePoint) - || codePoint == Constants.CODE_SINGLE_QUOTE - || codePoint == Constants.CODE_DOUBLE_QUOTE - || codePoint == Constants.CODE_CLOSING_PARENTHESIS - || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET - || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET - || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET - || codePoint == Constants.CODE_PLUS - || codePoint == Constants.CODE_PERCENT - || Character.getType(codePoint) == Character.OTHER_SYMBOL; } // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is @@ -1521,16 +1158,37 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return; } final String wordToEdit; - if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) { - wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); + if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) { + wordToEdit = word.toLowerCase(getCurrentSubtypeLocale()); } else { wordToEdit = word; } - mUserDictionary.addWordToUserDictionary(wordToEdit); + mInputLogic.mSuggest.mDictionaryFacilitator.addWordToUserDictionary(wordToEdit); + } + + // Callback for the {@link SuggestionStripView}, to call when the important notice strip is + // pressed. + @Override + public void showImportantNoticeContents() { + showOptionDialog(new ImportantNoticeDialog(this /* context */, this /* listener */)); + } + + // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener} + @Override + public void onClickSettingsOfImportantNoticeDialog(final int nextVersion) { + launchSettings(); } - private void onSettingsKeyPressed() { - if (isShowingOptionDialog()) return; + // Implement {@link ImportantNoticeDialog.ImportantNoticeDialogListener} + @Override + public void onUserAcknowledgmentOfImportantNoticeDialog(final int nextVersion) { + setNeutralSuggestionStrip(); + } + + public void displaySettingsDialog() { + if (isShowingOptionDialog()) { + return; + } showSubtypeSelectorAndSettings(); } @@ -1552,12 +1210,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen return mOptionsDialog != null && mOptionsDialog.isShowing(); } - private void performEditorAction(final int actionId) { - mConnection.performEditorAction(actionId); - } - // TODO: Revise the language switch key behavior to make it much smarter and more reasonable. - private void handleLanguageSwitchKey() { + public void switchToNextSubtype() { final IBinder token = getWindow().getWindow().getAttributes().token; if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) { mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */); @@ -1566,394 +1220,93 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen mSubtypeState.switchSubtype(token, mRichImm); } - private void sendDownUpKeyEvent(final int code) { - final long eventTime = SystemClock.uptimeMillis(); - mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, - KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, - KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); - mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, - KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, - KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); - } - - private void sendKeyCodePoint(final int code) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_sendKeyCodePoint(code); - } - // TODO: Remove this special handling of digit letters. - // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. - if (code >= '0' && code <= '9') { - sendDownUpKeyEvent(code - '0' + KeyEvent.KEYCODE_0); - return; - } - - if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) { - // 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. - sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER); - } else { - mConnection.commitText(StringUtils.newSingleCodePointString(code), 1); - } - } - // Implementation of {@link KeyboardActionListener}. @Override - public void onCodeInput(final int primaryCode, final int x, final int y) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onCodeInput(primaryCode, x, y); - } - final long when = SystemClock.uptimeMillis(); - if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) { - mDeleteCount = 0; - } - mLastKeyTime = when; - mConnection.beginBatchEdit(); - final KeyboardSwitcher switcher = mKeyboardSwitcher; - // The space state depends only on the last character pressed and its own previous - // state. Here, we revert the space state to neutral if the key is actually modifying - // the input contents (any non-shift key), which is what we should do for - // all inputs that do not result in a special state. Each character handling is then - // free to override the state as they see fit. - final int spaceState = mSpaceState; - if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false; - - // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state. - if (primaryCode != Constants.CODE_SPACE) { - mHandler.cancelDoubleSpacePeriodTimer(); - } - - boolean didAutoCorrect = false; - switch (primaryCode) { - case Constants.CODE_DELETE: - mSpaceState = SPACE_STATE_NONE; - handleBackspace(spaceState); - LatinImeLogger.logOnDelete(x, y); - break; - case Constants.CODE_SHIFT: - // Note: Calling back to the keyboard on Shift key is handled in - // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. - final Keyboard currentKeyboard = switcher.getKeyboard(); + public void onCodeInput(final int codePoint, final int x, final int y, + final boolean isKeyRepeat) { + 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. + 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()) { - // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for - // alphabetic shift and shift while in symbol layout. - handleRecapitalize(); - } - break; - case Constants.CODE_CAPSLOCK: - // Note: Changing keyboard to shift lock state is handled in - // {@link KeyboardSwitcher#onCodeInput(int)}. - break; - case Constants.CODE_SWITCH_ALPHA_SYMBOL: - // Note: Calling back to the keyboard on symbol key is handled in - // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. - break; - case Constants.CODE_SETTINGS: - onSettingsKeyPressed(); - break; - case Constants.CODE_SHORTCUT: - mSubtypeSwitcher.switchToShortcutIME(this); - break; - case Constants.CODE_ACTION_NEXT: - performEditorAction(EditorInfo.IME_ACTION_NEXT); - break; - case Constants.CODE_ACTION_PREVIOUS: - performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); - break; - case Constants.CODE_LANGUAGE_SWITCH: - handleLanguageSwitchKey(); - break; - case Constants.CODE_EMOJI: - // Note: Switching emoji keyboard is being handled in - // {@link KeyboardState#onCodeInput(int,int)}. - break; - case Constants.CODE_ENTER: - final EditorInfo editorInfo = getCurrentInputEditorInfo(); - final int imeOptionsActionId = - InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); - if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { - // Either we have an actionLabel and we should performEditorAction with actionId - // regardless of its value. - performEditorAction(editorInfo.actionId); - } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { - // We didn't have an actionLabel, but we had another action to execute. - // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, - // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it - // means there should be an action and the app didn't bother to set a specific - // code for it - presumably it only handles one. It does not have to be treated - // in any specific way: anything that is not IME_ACTION_NONE should be sent to - // performEditorAction. - performEditorAction(imeOptionsActionId); + codeToSend = codePoint; } else { - // No action label, and the action from imeOptions is NONE: this is a regular - // enter key that should input a carriage return. - didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); + codeToSend = Constants.CODE_SYMBOL_SHIFT; } - break; - case Constants.CODE_SHIFT_ENTER: - didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); - break; - default: - didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState); - break; - } - switcher.onCodeInput(primaryCode); - // Reset after any single keystroke, except shift, capslock, and symbol-shift - if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT - && primaryCode != Constants.CODE_CAPSLOCK - && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) - mLastComposedWord.deactivate(); - if (Constants.CODE_DELETE != primaryCode) { - mEnteredText = null; + } else { + codeToSend = codePoint; } - mConnection.endBatchEdit(); - } - - private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y, - final int spaceState) { - mSpaceState = SPACE_STATE_NONE; - final boolean didAutoCorrect; - final SettingsValues settingsValues = mSettings.getCurrent(); - if (settingsValues.isWordSeparator(primaryCode) - || Character.getType(primaryCode) == Character.OTHER_SYMBOL) { - didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState); + if (Constants.CODE_SHORTCUT == codePoint) { + 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(), mHandler); + updateStateAfterInputTransaction(completeInputTransaction); + mKeyboardSwitcher.onCodeInput(codePoint, 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, + final int keyY, final boolean isKeyRepeat) { + final int keyCode; + final int codePoint; + if (keyCodeOrCodePoint <= 0) { + keyCode = keyCodeOrCodePoint; + codePoint = Event.NOT_A_CODE_POINT; } else { - didAutoCorrect = false; - if (SPACE_STATE_PHANTOM == spaceState) { - if (settingsValues.mIsInternal) { - if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) { - LatinImeLoggerUtils.onAutoCorrection( - "", mWordComposer.getTypedWord(), " ", mWordComposer); - } - } - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can insert the character at the current cursor position. - resetEntireInputState(mLastSelectionStart); - } else { - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - } - } - final int keyX, keyY; - final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); - if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) { - keyX = x; - keyY = y; - } else { - keyX = Constants.NOT_A_COORDINATE; - keyY = Constants.NOT_A_COORDINATE; - } - handleCharacter(primaryCode, keyX, keyY, spaceState); + keyCode = Event.NOT_A_KEY_CODE; + codePoint = keyCodeOrCodePoint; } - mExpectingUpdateSelection = true; - return didAutoCorrect; + return Event.createSoftwareKeypressEvent(codePoint, keyCode, keyX, keyY, isKeyRepeat); } // Called from PointerTracker through the KeyboardActionListener interface @Override public void onTextInput(final String rawText) { - mConnection.beginBatchEdit(); - if (mWordComposer.isComposingWord()) { - commitCurrentAutoCorrection(rawText); - } else { - resetComposingState(true /* alsoResetLastComposedWord */); - } - mHandler.postUpdateSuggestionStrip(); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS - && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) { - ResearchLogger.getInstance().onResearchKeySelected(this); - return; - } - final String text = specificTldProcessingOnTextInput(rawText); - if (SPACE_STATE_PHANTOM == mSpaceState) { - promotePhantomSpace(); - } - mConnection.commitText(text, 1); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */); - } - mConnection.endBatchEdit(); - // Space state must be updated before calling updateShiftState - mSpaceState = SPACE_STATE_NONE; - mKeyboardSwitcher.updateShiftState(); - mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT); - mEnteredText = text; + // TODO: have the keyboard pass the correct key code when we need it. + final Event event = Event.createSoftwareTextEvent(rawText, Event.NOT_A_KEY_CODE); + mInputLogic.onTextInput(mSettings.getCurrent(), event, mHandler); + mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); + mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT, getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); } @Override public void onStartBatchInput() { - mInputUpdater.onStartBatchInput(); - mHandler.cancelUpdateSuggestionStrip(); - mConnection.beginBatchEdit(); - final SettingsValues settingsValues = mSettings.getCurrent(); - if (mWordComposer.isComposingWord()) { - if (settingsValues.mIsInternal) { - if (mWordComposer.isBatchMode()) { - LatinImeLoggerUtils.onAutoCorrection( - "", mWordComposer.getTypedWord(), " ", mWordComposer); - } - } - final int wordComposerSize = mWordComposer.size(); - // Since isComposingWord() is true, the size is at least 1. - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can insert the batch input at the current cursor position. - resetEntireInputState(mLastSelectionStart); - } else if (wordComposerSize <= 1) { - // We auto-correct the previous (typed, not gestured) string iff it's one character - // long. The reason for this is, even in the middle of gesture typing, you'll still - // tap one-letter words and you want them auto-corrected (typically, "i" in English - // should become "I"). However for any longer word, we assume that the reason for - // tapping probably is that the word you intend to type is not in the dictionary, - // so we do not attempt to correct, on the assumption that if that was a dictionary - // word, the user would probably have gestured instead. - commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR); - } else { - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - } - mExpectingUpdateSelection = true; - } - final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); - if (Character.isLetterOrDigit(codePointBeforeCursor) - || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) { - mSpaceState = SPACE_STATE_PHANTOM; - } - mConnection.endBatchEdit(); - mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); + mInputLogic.onStartBatchInput(mSettings.getCurrent(), mKeyboardSwitcher, mHandler); } - static final class InputUpdater implements Handler.Callback { - private final Handler mHandler; - private final LatinIME mLatinIme; - private final Object mLock = new Object(); - private boolean mInBatchInput; // synchronized using {@link #mLock}. - - InputUpdater(final LatinIME latinIme) { - final HandlerThread handlerThread = new HandlerThread( - InputUpdater.class.getSimpleName()); - handlerThread.start(); - mHandler = new Handler(handlerThread.getLooper(), this); - mLatinIme = latinIme; - } - - private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1; - private static final int MSG_GET_SUGGESTED_WORDS = 2; - - @Override - public boolean handleMessage(final Message msg) { - // TODO: straighten message passing - we don't need two kinds of messages calling - // each other. - switch (msg.what) { - case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP: - updateBatchInput((InputPointers)msg.obj, msg.arg2 /* sequenceNumber */); - break; - case MSG_GET_SUGGESTED_WORDS: - mLatinIme.getSuggestedWords(msg.arg1 /* sessionId */, - msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj); - break; - } - return true; - } - - // Run in the UI thread. - public void onStartBatchInput() { - synchronized (mLock) { - mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); - mInBatchInput = true; - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); - } - } - - // Run in the Handler thread. - private void updateBatchInput(final InputPointers batchPointers, final int sequenceNumber) { - synchronized (mLock) { - if (!mInBatchInput) { - // Batch input has ended or canceled while the message was being delivered. - return; - } - - getSuggestedWordsGestureLocked(batchPointers, sequenceNumber, - new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(final SuggestedWords suggestedWords) { - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - suggestedWords, false /* dismissGestureFloatingPreviewText */); - } - }); - } - } - - // Run in the UI thread. - public void onUpdateBatchInput(final InputPointers batchPointers, - final int sequenceNumber) { - if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) { - return; - } - mHandler.obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, 0 /* arg1 */, - sequenceNumber /* arg2 */, batchPointers /* obj */).sendToTarget(); - } - - public void onCancelBatchInput() { - synchronized (mLock) { - mInBatchInput = false; - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip( - SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); - } - } - - // Run in the UI thread. - public void onEndBatchInput(final InputPointers batchPointers) { - synchronized(mLock) { - getSuggestedWordsGestureLocked(batchPointers, SuggestedWords.NOT_A_SEQUENCE_NUMBER, - new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(final SuggestedWords suggestedWords) { - mInBatchInput = false; - mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords, - true /* dismissGestureFloatingPreviewText */); - mLatinIme.mHandler.onEndBatchInput(suggestedWords); - } - }); - } - } - - // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to - // be synchronized. - private void getSuggestedWordsGestureLocked(final InputPointers batchPointers, - final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { - mLatinIme.mWordComposer.setBatchInputPointers(batchPointers); - mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE, - sequenceNumber, new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(SuggestedWords suggestedWords) { - final int suggestionCount = suggestedWords.size(); - if (suggestionCount <= 1) { - final String mostProbableSuggestion = (suggestionCount == 0) ? null - : suggestedWords.getWord(0); - callback.onGetSuggestedWords( - mLatinIme.getOlderSuggestions(mostProbableSuggestion)); - } - callback.onGetSuggestedWords(suggestedWords); - } - }); - } + @Override + public void onUpdateBatchInput(final InputPointers batchPointers) { + mInputLogic.onUpdateBatchInput(mSettings.getCurrent(), batchPointers, mKeyboardSwitcher); + } - public void getSuggestedWords(final int sessionId, final int sequenceNumber, - final OnGetSuggestedWordsCallback callback) { - mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback) - .sendToTarget(); - } + @Override + public void onEndBatchInput(final InputPointers batchPointers) { + mInputLogic.onEndBatchInput(batchPointers); + } - void quitLooper() { - mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS); - mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP); - mHandler.getLooper().quit(); - } + @Override + public void onCancelBatchInput() { + mInputLogic.onCancelBatchInput(mHandler); } - // This method must run in UI Thread. + // This method must run on the UI Thread. private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords, final boolean dismissGestureFloatingPreviewText) { showSuggestionStrip(suggestedWords); @@ -1964,107 +1317,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } - /* The sequence number member is only used in onUpdateBatchInput. It is increased each time - * auto-commit happens. The reason we need this is, when auto-commit happens we trim the - * input pointers that are held in a singleton, and to know how much to trim we rely on the - * results of the suggestion process that is held in mSuggestedWords. - * However, the suggestion process is asynchronous, and sometimes we may enter the - * onUpdateBatchInput method twice without having recomputed suggestions yet, or having - * received new suggestions generated from not-yet-trimmed input pointers. In this case, the - * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we - * remove an unrelated number of pointers (possibly even more than are left in the input - * pointers, leading to a crash). - * To avoid that, we increase the sequence number each time we auto-commit and trim the - * input pointers, and we do not use any suggested words that have been generated with an - * earlier sequence number. - */ - private int mAutoCommitSequenceNumber = 1; - @Override - public void onUpdateBatchInput(final InputPointers batchPointers) { - if (mSettings.getCurrent().mPhraseGestureEnabled) { - final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate(); - // If these suggested words have been generated with out of date input pointers, then - // we skip auto-commit (see comments above on the mSequenceNumber member). - if (null != candidate && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) { - if (candidate.mSourceDict.shouldAutoCommit(candidate)) { - final String[] commitParts = candidate.mWord.split(" ", 2); - batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); - promotePhantomSpace(); - mConnection.commitText(commitParts[0], 0); - mSpaceState = SPACE_STATE_PHANTOM; - mKeyboardSwitcher.updateShiftState(); - mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); - ++mAutoCommitSequenceNumber; - } - } - } - mInputUpdater.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber); - } - - // This method must run in UI Thread. - public void onEndBatchInputAsyncInternal(final SuggestedWords suggestedWords) { - final String batchInputText = suggestedWords.isEmpty() - ? null : suggestedWords.getWord(0); - if (TextUtils.isEmpty(batchInputText)) { - return; - } - mConnection.beginBatchEdit(); - if (SPACE_STATE_PHANTOM == mSpaceState) { - promotePhantomSpace(); - } - if (mSettings.getCurrent().mPhraseGestureEnabled) { - // Find the last space - final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; - if (0 != indexOfLastSpace) { - mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1); - showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture()); - } - final String lastWord = batchInputText.substring(indexOfLastSpace); - mWordComposer.setBatchInputWord(lastWord); - mConnection.setComposingText(lastWord, 1); - } else { - mWordComposer.setBatchInputWord(batchInputText); - mConnection.setComposingText(batchInputText, 1); - } - mExpectingUpdateSelection = true; - mConnection.endBatchEdit(); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords); - } - // Space state must be updated before calling updateShiftState - mSpaceState = SPACE_STATE_PHANTOM; - mKeyboardSwitcher.updateShiftState(); - } - - @Override - public void onEndBatchInput(final InputPointers batchPointers) { - mInputUpdater.onEndBatchInput(batchPointers); - } - - private String specificTldProcessingOnTextInput(final String text) { - if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD - || !Character.isLetter(text.charAt(1))) { - // Not a tld: do nothing. - return text; - } - // We have a TLD (or something that looks like this): make sure we don't add - // a space even if currently in phantom mode. - mSpaceState = SPACE_STATE_NONE; - // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code - final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0); - if (lastOne != null && lastOne.length() == 1 - && lastOne.charAt(0) == Constants.CODE_PERIOD) { - return text.substring(1); - } else { - return text; - } - } - // Called from PointerTracker through the KeyboardActionListener interface @Override public void onFinishSlidingInput() { // User finished sliding input. - mKeyboardSwitcher.onFinishSlidingInput(); + mKeyboardSwitcher.onFinishSlidingInput(getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); } // Called from PointerTracker through the KeyboardActionListener interface @@ -2074,476 +1332,85 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // Nothing to do so far. } - @Override - public void onCancelBatchInput() { - mInputUpdater.onCancelBatchInput(); - } - - private void handleBackspace(final int spaceState) { - // We revert these in this method if the deletion doesn't happen. - mDeleteCount++; - mExpectingUpdateSelection = true; - - // In many cases, we may have to put the keyboard in auto-shift state again. However - // we want to wait a few milliseconds before doing it to avoid the keyboard flashing - // during key repeat. - mHandler.postUpdateShiftState(); - - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can remove the character at the current cursor position. - resetEntireInputState(mLastSelectionStart); - // When we exit this if-clause, mWordComposer.isComposingWord() will return false. - } - if (mWordComposer.isComposingWord()) { - if (mWordComposer.isBatchMode()) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - final String word = mWordComposer.getTypedWord(); - ResearchLogger.latinIME_handleBackspace_batch(word, 1); - } - final String rejectedSuggestion = mWordComposer.getTypedWord(); - mWordComposer.reset(); - mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); - } else { - mWordComposer.deleteLast(); - } - mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); - mHandler.postUpdateSuggestionStrip(); - if (!mWordComposer.isComposingWord()) { - // If we just removed the last character, auto-caps mode may have changed so we - // need to re-evaluate. - mKeyboardSwitcher.updateShiftState(); - } - } else { - final SettingsValues currentSettings = mSettings.getCurrent(); - if (mLastComposedWord.canRevertCommit()) { - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onAutoCorrectionCancellation(); - } - revertCommit(); - return; - } - if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { - // Cancel multi-character input: remove the text we just entered. - // 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); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText); - } - 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 - // reverting any autocorrect at this point. So we can safely return. - return; - } - if (SPACE_STATE_DOUBLE == spaceState) { - mHandler.cancelDoubleSpacePeriodTimer(); - if (mConnection.revertDoubleSpacePeriod()) { - // No need to reset mSpaceState, it has already be done (that's why we - // receive it as a parameter) - return; - } - } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) { - if (mConnection.revertSwapPunctuation()) { - // Likewise - return; - } - } - - // No cancelling of commit/double space/swap: we have a regular backspace. - // We should backspace one char and restart suggestion if at the end of a word. - if (mLastSelectionStart != mLastSelectionEnd) { - // If there is a selection, remove it. - final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; - mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); - // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to - // happen, and if it's wrong, the next call to onUpdateSelection will correct it, - // but we want to set it right away to avoid it being used with the wrong values - // later (typically, in a subsequent press on backspace). - mLastSelectionEnd = mLastSelectionStart; - mConnection.deleteSurroundingText(numCharsDeleted, 0); - } else { - // There is no selection, just delete one character. - if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) { - // This should never happen. - Log.e(TAG, "Backspace when we don't know the selection position"); - } - if (mAppWorkAroundsUtils.isBeforeJellyBean() || - currentSettings.mInputAttributes.isTypeNull()) { - // There are two 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. - sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); - if (mDeleteCount > DELETE_ACCELERATE_AT) { - sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); - } - } else { - final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); - if (codePointBeforeCursor == Constants.NOT_A_CODE) { - // Nothing to delete before the cursor. We have to revert the deletion - // states that were updated at the beginning of this method. - mDeleteCount--; - mExpectingUpdateSelection = false; - return; - } - final int lengthToDelete = - Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; - mConnection.deleteSurroundingText(lengthToDelete, 0); - if (mDeleteCount > DELETE_ACCELERATE_AT) { - final int codePointBeforeCursorToDeleteAgain = - mConnection.getCodePointBeforeCursor(); - if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { - final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( - codePointBeforeCursorToDeleteAgain) ? 2 : 1; - mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); - } - } - } - } - if (currentSettings.isSuggestionsRequested(mDisplayOrientation) - && currentSettings.mCurrentLanguageHasSpaces) { - restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(); - } - // We just removed a character. We need to update the auto-caps state. - mKeyboardSwitcher.updateShiftState(); - } - } - - /* - * Strip a trailing space if necessary and returns whether it's a swap weak space situation. - */ - private boolean maybeStripSpace(final int code, - final int spaceState, final boolean isFromSuggestionStrip) { - if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) { - mConnection.removeTrailingSpace(); + private boolean isSuggestionStripVisible() { + if (!hasSuggestionStripView()) { return false; } - if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState) - && isFromSuggestionStrip) { - final SettingsValues currentSettings = mSettings.getCurrent(); - if (currentSettings.isUsuallyPrecededBySpace(code)) return false; - if (currentSettings.isUsuallyFollowedBySpace(code)) return true; - mConnection.removeTrailingSpace(); - } - return false; - } - - private void handleCharacter(final int primaryCode, final int x, - final int y, final int spaceState) { - // TODO: refactor this method to stop flipping isComposingWord around all the time, and - // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter - // which has the same name as other handle* methods but is not the same. - boolean isComposingWord = mWordComposer.isComposingWord(); - - // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. - // See onStartBatchInput() to see how to do it. - final SettingsValues currentSettings = mSettings.getCurrent(); - if (SPACE_STATE_PHANTOM == spaceState && !currentSettings.isWordConnector(primaryCode)) { - if (isComposingWord) { - // Sanity check - throw new RuntimeException("Should not be composing here"); - } - promotePhantomSpace(); - } - - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can insert the character at the current cursor position. - resetEntireInputState(mLastSelectionStart); - isComposingWord = false; - } - // We want to find out whether to start composing a new word with this character. If so, - // we need to reset the composing state and switch isComposingWord. The order of the - // tests is important for good performance. - // We only start composing if we're not already composing. - if (!isComposingWord - // We only start composing if this is a word code point. Essentially that means it's a - // a letter or a word connector. - && currentSettings.isWordCodePoint(primaryCode) - // We never go into composing state if suggestions are not requested. - && currentSettings.isSuggestionsRequested(mDisplayOrientation) && - // In languages with spaces, we only start composing a word when we are not already - // touching a word. In languages without spaces, the above conditions are sufficient. - (!mConnection.isCursorTouchingWord(currentSettings) - || !currentSettings.mCurrentLanguageHasSpaces)) { - // Reset entirely the composing state anyway, then start composing a new word unless - // the character is a single quote or a dash. The idea here is, single quote and dash - // are not separators and they should be treated as normal characters, except in the - // first position where they should not start composing a word. - isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode - && Constants.CODE_DASH != primaryCode); - // Here we don't need to reset the last composed word. It will be reset - // when we commit this one, if we ever do; if on the other hand we backspace - // it entirely and resume suggestions on the previous word, we'd like to still - // have touch coordinates for it. - resetComposingState(false /* alsoResetLastComposedWord */); - } - if (isComposingWord) { - final int keyX, keyY; - if (Constants.isValidCoordinate(x) && Constants.isValidCoordinate(y)) { - final KeyDetector keyDetector = - mKeyboardSwitcher.getMainKeyboardView().getKeyDetector(); - keyX = keyDetector.getTouchX(x); - keyY = keyDetector.getTouchY(y); - } else { - keyX = x; - keyY = y; - } - mWordComposer.add(primaryCode, keyX, keyY); - // If it's the first letter, make note of auto-caps state - if (mWordComposer.size() == 1) { - mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode()); - } - mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); - } else { - final boolean swapWeakSpace = maybeStripSpace(primaryCode, - spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x); - - sendKeyCodePoint(primaryCode); - - if (swapWeakSpace) { - swapSwapperAndSpace(); - mSpaceState = SPACE_STATE_WEAK; - } - // In case the "add to dictionary" hint was still displayed. - if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint(); - } - mHandler.postUpdateSuggestionStrip(); - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y); - } - } - - private void handleRecapitalize() { - if (mLastSelectionStart == mLastSelectionEnd) return; // No selection - // If we have a recapitalize in progress, use it; otherwise, create a new one. - if (!mRecapitalizeStatus.isActive() - || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { - final CharSequence selectedText = - mConnection.getSelectedText(0 /* flags, 0 for no styles */); - if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection - final SettingsValues currentSettings = mSettings.getCurrent(); - mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd, - selectedText.toString(), currentSettings.mLocale, - currentSettings.mWordSeparators); - // We trim leading and trailing whitespace. - mRecapitalizeStatus.trim(); - // Trimming the object may have changed the length of the string, and we need to - // reposition the selection handles accordingly. As this result in an IPC call, - // only do it if it's actually necessary, in other words if the recapitalize status - // is not set at the same place as before. - if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) { - mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); - mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); - } + if (mSuggestionStripView.isShowingAddToDictionaryHint()) { + return true; } - mConnection.finishComposingText(); - mRecapitalizeStatus.rotate(); - final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart; - mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd); - mConnection.deleteSurroundingText(numCharsDeleted, 0); - mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); - mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart(); - mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd(); - mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd); - // Match the keyboard to the new state. - mKeyboardSwitcher.updateShiftState(); - } - - // Returns true if we do an autocorrection, false otherwise. - private boolean handleSeparator(final int primaryCode, final int x, final int y, - final int spaceState) { - boolean didAutoCorrect = false; final SettingsValues currentSettings = mSettings.getCurrent(); - // We avoid sending spaces in languages without spaces if we were composing. - final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode - && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord(); - if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { - // If we are in the middle of a recorrection, we need to commit the recorrection - // first so that we can insert the separator at the current cursor position. - resetEntireInputState(mLastSelectionStart); - } - if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing - if (currentSettings.mCorrectionEnabled) { - final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR - : StringUtils.newSingleCodePointString(primaryCode); - commitCurrentAutoCorrection(separator); - didAutoCorrect = true; - } else { - commitTyped(StringUtils.newSingleCodePointString(primaryCode)); - } - } - - final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState, - Constants.SUGGESTION_STRIP_COORDINATE == x); - - if (SPACE_STATE_PHANTOM == spaceState && - currentSettings.isUsuallyPrecededBySpace(primaryCode)) { - promotePhantomSpace(); - } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord()); - } - - if (!shouldAvoidSendingCode) { - sendKeyCodePoint(primaryCode); + if (null == currentSettings) { + return false; } - - if (Constants.CODE_SPACE == primaryCode) { - if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) { - if (maybeDoubleSpacePeriod()) { - mSpaceState = SPACE_STATE_DOUBLE; - } else if (!isShowingPunctuationList()) { - mSpaceState = SPACE_STATE_WEAK; - } - } - - mHandler.startDoubleSpacePeriodTimer(); - mHandler.postUpdateSuggestionStrip(); - } else { - if (swapWeakSpace) { - swapSwapperAndSpace(); - mSpaceState = SPACE_STATE_SWAP_PUNCTUATION; - } else if (SPACE_STATE_PHANTOM == spaceState - && currentSettings.isUsuallyFollowedBySpace(primaryCode)) { - // If we are in phantom space state, and the user presses a separator, we want to - // stay in phantom space state so that the next keypress has a chance to add the - // space. For example, if I type "Good dat", pick "day" from the suggestion strip - // then insert a comma and go on to typing the next word, I want the space to be - // inserted automatically before the next word, the same way it is when I don't - // input the comma. - // The case is a little different if the separator is a space stripper. Such a - // separator does not normally need a space on the right (that's the difference - // between swappers and strippers), so we should not stay in phantom space state if - // the separator is a stripper. Hence the additional test above. - mSpaceState = SPACE_STATE_PHANTOM; - } - - // Set punctuation right away. onUpdateSelection will fire but tests whether it is - // already displayed or not, so it's okay. - setPunctuationSuggestions(); + if (ImportantNoticeUtils.shouldShowImportantNotice(this, + currentSettings.mInputAttributes)) { + return true; } - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y); + if (!currentSettings.isSuggestionStripVisible()) { + return false; } - - mKeyboardSwitcher.updateShiftState(); - return didAutoCorrect; - } - - private CharSequence getTextWithUnderline(final String text) { - return mIsAutoCorrectionIndicatorOn - ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text) - : text; - } - - private void handleClose() { - // TODO: Verify that words are logged properly when IME is closed. - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - requestHideSelf(0); - final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); - if (mainKeyboardView != null) { - mainKeyboardView.closing(); + if (currentSettings.isApplicationSpecifiedCompletionsOn()) { + return true; } + return currentSettings.isSuggestionsRequested(); } - // TODO: make this private - // Outside LatinIME, only used by the test suite. - @UsedForTesting - boolean isShowingPunctuationList() { - if (mSuggestedWords == null) return false; - return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords; - } - - private boolean isSuggestionsStripVisible() { - final SettingsValues currentSettings = mSettings.getCurrent(); - if (mSuggestionStripView == null) - return false; - if (mSuggestionStripView.isShowingAddToDictionaryHint()) - return true; - if (null == currentSettings) - return false; - if (!currentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation)) - return false; - if (currentSettings.isApplicationSpecifiedCompletionsOn()) - return true; - return currentSettings.isSuggestionsRequested(mDisplayOrientation); + public boolean hasSuggestionStripView() { + return null != mSuggestionStripView; } - private void clearSuggestionStrip() { - setSuggestedWords(SuggestedWords.EMPTY, false); - setAutoCorrectionIndicator(false); + @Override + public boolean isShowingAddToDictionaryHint() { + return hasSuggestionStripView() && mSuggestionStripView.isShowingAddToDictionaryHint(); } - private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) { - mSuggestedWords = words; - if (mSuggestionStripView != null) { - mSuggestionStripView.setSuggestions(words); - mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); + @Override + public void dismissAddToDictionaryHint() { + if (!hasSuggestionStripView()) { + return; } + mSuggestionStripView.dismissAddToDictionaryHint(); } - private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) { - // Put a blue underline to a word in TextView which will be auto-corrected. - if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator - && mWordComposer.isComposingWord()) { - mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; - final CharSequence textWithUnderline = - getTextWithUnderline(mWordComposer.getTypedWord()); - // TODO: when called from an updateSuggestionStrip() call that results from a posted - // message, this is called outside any batch edit. Potentially, this may result in some - // janky flickering of the screen, although the display speed makes it unlikely in - // the practice. - mConnection.setComposingText(textWithUnderline, 1); + // TODO[IL]: Define a clear interface for this + public void setSuggestedWords(final SuggestedWords suggestedWords, + final boolean isSuggestionStripVisible) { + mInputLogic.setSuggestedWords(suggestedWords); + // TODO: Modify this when we support suggestions with hard keyboard + if (!hasSuggestionStripView()) { + return; } - } - - private void updateSuggestionStrip() { - mHandler.cancelUpdateSuggestionStrip(); - final SettingsValues currentSettings = mSettings.getCurrent(); - - // Check if we have a suggestion engine attached. - if (mSuggest == null - || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) { - if (mWordComposer.isComposingWord()) { - Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " - + "requested!"); - } + mKeyboardSwitcher.onAutoCorrectionStateChanged(suggestedWords.mWillAutoCorrect); + if (!onEvaluateInputViewShown()) { return; } - - if (!mWordComposer.isComposingWord() && !currentSettings.mBigramPredictionEnabled) { - setPunctuationSuggestions(); + if (!isSuggestionStripVisible) { + mSuggestionStripView.setVisibility(isFullscreenMode() ? View.GONE : View.INVISIBLE); return; } + mSuggestionStripView.setVisibility(View.VISIBLE); - final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>(); - getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING, - SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(final SuggestedWords suggestedWords) { - holder.set(suggestedWords); - } - } - ); - - // This line may cause the current thread to wait. - final SuggestedWords suggestedWords = holder.get(null, GET_SUGGESTED_WORDS_TIMEOUT); - if (suggestedWords != null) { - showSuggestionStrip(suggestedWords); + final SettingsValues currentSettings = mSettings.getCurrent(); + final boolean showSuggestions; + if (SuggestedWords.EMPTY == suggestedWords || suggestedWords.isPunctuationSuggestions() + || !currentSettings.isSuggestionsRequested()) { + showSuggestions = !mSuggestionStripView.maybeShowImportantNoticeTitle( + currentSettings.mInputAttributes); + } else { + showSuggestions = true; + } + if (showSuggestions) { + mSuggestionStripView.setSuggestions(suggestedWords, + SubtypeLocaleUtils.isRtlLanguage(mSubtypeSwitcher.getCurrentSubtype())); } } - private void getSuggestedWords(final int sessionId, final int sequenceNumber, + // TODO[IL]: Move this out of LatinIME. + public void getSuggestedWords(final int sessionId, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); - final Suggest suggest = mSuggest; - if (keyboard == null || suggest == null) { + if (keyboard == null) { callback.onGetSuggestedWords(SuggestedWords.EMPTY); return; } @@ -2552,528 +1419,83 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // should just skip whitespace if any, so 1. final SettingsValues currentSettings = mSettings.getCurrent(); final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues; - final String prevWord; - if (currentSettings.mCurrentLanguageHasSpaces) { - // If we are typing in a language with spaces we can just look up the previous - // word from textview. - prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, - mWordComposer.isComposingWord() ? 2 : 1); - } else { - prevWord = LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null - : mLastComposedWord.mCommittedWord; - } - suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(), - currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled, - additionalFeaturesOptions, sessionId, sequenceNumber, callback); - } - - private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId, - final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { - mInputUpdater.getSuggestedWords(sessionId, sequenceNumber, - new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords(SuggestedWords suggestedWords) { - callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions( - mWordComposer.getTypedWord(), suggestedWords)); - } - }); - } - - private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord, - final SuggestedWords suggestedWords) { - // TODO: consolidate this into getSuggestedWords - // We update the suggestion strip only when we have some suggestions to show, i.e. when - // the suggestion count is > 1; else, we leave the old suggestions, with the typed word - // replaced with the new one. However, when the word is a dictionary word, or when the - // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the - // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to - // revert to suggestions - although it is unclear how we can come here if it's displayed. - if (suggestedWords.size() > 1 || typedWord.length() <= 1 - || suggestedWords.mTypedWordValid || null == mSuggestionStripView - || mSuggestionStripView.isShowingAddToDictionaryHint()) { - return suggestedWords; - } else { - return getOlderSuggestions(typedWord); - } - } - private SuggestedWords getOlderSuggestions(final String typedWord) { - SuggestedWords previousSuggestedWords = mSuggestedWords; - if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) { - previousSuggestedWords = SuggestedWords.EMPTY; - } - if (typedWord == null) { - return previousSuggestedWords; + if (DEBUG) { + if (mInputLogic.mWordComposer.isComposingWord() + || mInputLogic.mWordComposer.isBatchMode()) { + final String previousWord + = mInputLogic.mWordComposer.getPreviousWordForSuggestion(); + // TODO: this is for checking consistency with older versions. Remove this when + // we are confident this is stable. + // We're checking the previous word in the text field against the memorized previous + // word. If we are composing a word we should have the second word before the cursor + // memorized, otherwise we should have the first. + final CharSequence rereadPrevWord = mInputLogic.getNthPreviousWordForSuggestion( + currentSettings.mSpacingAndPunctuations, + mInputLogic.mWordComposer.isComposingWord() ? 2 : 1); + if (!TextUtils.equals(previousWord, rereadPrevWord)) { + throw new RuntimeException("Unexpected previous word: " + + previousWord + " <> " + rereadPrevWord); + } + } } - final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = - SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, - previousSuggestedWords); - return new SuggestedWords(typedWordAndPreviousSuggestions, - false /* typedWordValid */, - false /* hasAutoCorrectionCandidate */, - false /* isPunctuationSuggestions */, - true /* isObsoleteSuggestions */, - false /* isPrediction */); + mInputLogic.mSuggest.getSuggestedWords(mInputLogic.mWordComposer, + mInputLogic.mWordComposer.getPreviousWordForSuggestion(), + keyboard.getProximityInfo(), currentSettings.mBlockPotentiallyOffensive, + currentSettings.mCorrectionEnabled, additionalFeaturesOptions, sessionId, + sequenceNumber, callback); } - private void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) { - if (suggestedWords.isEmpty()) return; + @Override + public void showSuggestionStrip(final SuggestedWords sourceSuggestedWords) { + final SuggestedWords suggestedWords = + sourceSuggestedWords.isEmpty() ? SuggestedWords.EMPTY : sourceSuggestedWords; final String autoCorrection; if (suggestedWords.mWillAutoCorrect) { autoCorrection = suggestedWords.getWord(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 = typedWord; - } - mWordComposer.setAutoCorrection(autoCorrection); - } - - private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords, - final String typedWord) { - if (suggestedWords.isEmpty()) { - // No auto-correction is available, clear the cached values. - AccessibilityUtils.getInstance().setAutoCorrection(null, null); - clearSuggestionStrip(); - return; - } - setAutoCorrection(suggestedWords, typedWord); - final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); - setSuggestedWords(suggestedWords, isAutoCorrection); - setAutoCorrectionIndicator(isAutoCorrection); - setSuggestionStripShown(isSuggestionsStripVisible()); - // An auto-correction is available, cache it in accessibility code so - // we can be speak it if the user touches a key that will insert it. - AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord); - } - - private void showSuggestionStrip(final SuggestedWords suggestedWords) { - if (suggestedWords.isEmpty()) { - clearSuggestionStrip(); - return; + autoCorrection = sourceSuggestedWords.mTypedWord; } - showSuggestionStripWithTypedWord(suggestedWords, - suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)); - } - - private void commitCurrentAutoCorrection(final String separator) { - // Complete any pending suggestions query first - if (mHandler.hasPendingUpdateSuggestions()) { - updateSuggestionStrip(); - } - final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); - final String typedWord = mWordComposer.getTypedWord(); - final String autoCorrection = (typedAutoCorrection != null) - ? typedAutoCorrection : typedWord; - if (autoCorrection != null) { - if (TextUtils.isEmpty(typedWord)) { - throw new RuntimeException("We have an auto-correction but the typed word " - + "is empty? Impossible! I must commit suicide."); - } - if (mSettings.isInternal()) { - LatinImeLoggerUtils.onAutoCorrection( - typedWord, autoCorrection, separator, mWordComposer); - } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - final SuggestedWords suggestedWords = mSuggestedWords; - ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection, - separator, mWordComposer.isBatchMode(), suggestedWords); - } - mExpectingUpdateSelection = true; - commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD, - separator); - if (!typedWord.equals(autoCorrection)) { - // 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 - // the segment of text starting at the supplied index and running for the length - // of the auto-correction flash. At this moment, the "typedWord" argument is - // ignored by TextView. - mConnection.commitCorrection( - new CorrectionInfo(mLastSelectionEnd - typedWord.length(), - typedWord, autoCorrection)); - } + if (SuggestedWords.EMPTY == suggestedWords) { + setNeutralSuggestionStrip(); + } else { + mInputLogic.mWordComposer.setAutoCorrection(autoCorrection); + setSuggestedWords(suggestedWords, isSuggestionStripVisible()); } + // 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); } // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} // interface @Override public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) { - final SuggestedWords suggestedWords = mSuggestedWords; - final String suggestion = suggestionInfo.mWord; - // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput - if (suggestion.length() == 1 && isShowingPunctuationList()) { - // Word separators are suggested before the user inputs something. - // So, LatinImeLogger logs "" as a user's input. - LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords); - // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. - final int primaryCode = suggestion.charAt(0); - onCodeInput(primaryCode, - Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, - false /* isBatchMode */, suggestedWords.mIsPrediction); - } - return; - } - - mConnection.beginBatchEdit(); - final SettingsValues currentSettings = mSettings.getCurrent(); - if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0 - // In the batch input mode, a manually picked suggested word should just replace - // the current batch input text and there is no need for a phantom space. - && !mWordComposer.isBatchMode()) { - final int firstChar = Character.codePointAt(suggestion, 0); - if (!currentSettings.isWordSeparator(firstChar) - || currentSettings.isUsuallyPrecededBySpace(firstChar)) { - promotePhantomSpace(); - } - } - - if (currentSettings.isApplicationSpecifiedCompletionsOn() - && mApplicationSpecifiedCompletions != null - && index >= 0 && index < mApplicationSpecifiedCompletions.length) { - mSuggestedWords = SuggestedWords.EMPTY; - if (mSuggestionStripView != null) { - mSuggestionStripView.clear(); - } - mKeyboardSwitcher.updateShiftState(); - resetComposingState(true /* alsoResetLastComposedWord */); - final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index]; - mConnection.commitCompletion(completionInfo); - mConnection.endBatchEdit(); - return; - } - - // We need to log before we commit, because the word composer will store away the user - // typed word. - final String replacedWord = mWordComposer.getTypedWord(); - LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); - mExpectingUpdateSelection = true; - commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, - LastComposedWord.NOT_A_SEPARATOR); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, - mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind, - suggestionInfo.mSourceDict.mDictType); - } - mConnection.endBatchEdit(); - // Don't allow cancellation of manual pick - mLastComposedWord.deactivate(); - // Space state must be updated before calling updateShiftState - mSpaceState = SPACE_STATE_PHANTOM; - mKeyboardSwitcher.updateShiftState(); - - // We should show the "Touch again to save" hint if the user pressed the first entry - // AND it's in none of our current dictionaries (main, user or otherwise). - // Please note that if mSuggest is null, it means that everything is off: suggestion - // and correction, so we shouldn't try to show the hint - final Suggest suggest = mSuggest; - final boolean showingAddToDictionaryHint = - (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind - || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind) - && suggest != null - // If the suggestion is not in the dictionary, the hint should be shown. - && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true); - - if (currentSettings.mIsInternal) { - LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); - } - if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) { - mSuggestionStripView.showAddToDictionaryHint( - suggestion, currentSettings.mHintToSaveText); - } else { - // If we're not showing the "Touch again to save", then update the suggestion strip. - mHandler.postUpdateSuggestionStrip(); - } + final InputTransaction completeInputTransaction = mInputLogic.onPickSuggestionManually( + mSettings.getCurrent(), index, suggestionInfo, + mKeyboardSwitcher.getKeyboardShiftMode(), mHandler); + updateStateAfterInputTransaction(completeInputTransaction); } - /** - * Commits the chosen word to the text field and saves it for later retrieval. - */ - private void commitChosenWord(final String chosenWord, final int commitType, - final String separatorString) { - final SuggestedWords suggestedWords = mSuggestedWords; - mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( - this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1); - // Add the word to the user history dictionary - final String prevWord = addToUserHistoryDictionary(chosenWord); - // 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, chosenWord, separatorString, - prevWord); - } - - private void setPunctuationSuggestions() { - final SettingsValues currentSettings = mSettings.getCurrent(); - if (currentSettings.mBigramPredictionEnabled) { - clearSuggestionStrip(); - } else { - setSuggestedWords(currentSettings.mSuggestPuncList, false); - } - setAutoCorrectionIndicator(false); - setSuggestionStripShown(isSuggestionsStripVisible()); - } - - private String addToUserHistoryDictionary(final String suggestion) { - if (TextUtils.isEmpty(suggestion)) return null; - final Suggest suggest = mSuggest; - if (suggest == null) return null; - - // 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. - final SettingsValues currentSettings = mSettings.getCurrent(); - if (!currentSettings.mCorrectionEnabled) return null; - - final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary; - if (userHistoryDictionary == null) return null; - - final String prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, 2); - final String secondWord; - if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) { - secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale()); - } else { - secondWord = suggestion; - } - // 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 int maxFreq = AutoCorrectionUtils.getMaxFrequency( - suggest.getUnigramDictionaries(), suggestion); - if (maxFreq == 0) return null; - userHistoryDictionary.addToDictionary(prevWord, secondWord, maxFreq > 0); - return prevWord; - } - - private boolean isResumableWord(final String word, final SettingsValues settings) { - final int firstCodePoint = word.codePointAt(0); - return settings.isWordCodePoint(firstCodePoint) - && Constants.CODE_SINGLE_QUOTE != firstCodePoint - && Constants.CODE_DASH != firstCodePoint; - } - - /** - * Check if the cursor is touching a word. If so, restart suggestions on this word, else - * do nothing. - */ - private void restartSuggestionsOnWordTouchedByCursor() { - // HACK: We may want to special-case some apps that exhibit bad behavior in case of - // recorrection. This is a temporary, stopgap measure that will be removed later. - // TODO: remove this. - if (mAppWorkAroundsUtils.isBrokenByRecorrection()) return; - // A simple way to test for support from the TextView. - if (!isSuggestionsStripVisible()) return; - // Recorrection is not supported in languages without spaces because we don't know - // how to segment them yet. - if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return; - // If the cursor is not touching a word, or if there is a selection, return right away. - if (mLastSelectionStart != mLastSelectionEnd) return; - // If we don't know the cursor location, return. - if (mLastSelectionStart < 0) return; - final SettingsValues currentSettings = mSettings.getCurrent(); - if (!mConnection.isCursorTouchingWord(currentSettings)) return; - final TextRange range = mConnection.getWordRangeAtCursor(currentSettings.mWordSeparators, - 0 /* additionalPrecedingWordsCount */); - if (null == range) return; // Happens if we don't have an input connection at all - if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out. - // If for some strange reason (editor bug or so) we measure the text before the cursor as - // longer than what the entire text is supposed to be, the safe thing to do is bail out. - final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); - if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return; - final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); - final String typedWord = range.mWord.toString(); - if (!isResumableWord(typedWord, currentSettings)) return; - int i = 0; - for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) { - for (final String s : span.getSuggestions()) { - ++i; - if (!TextUtils.equals(s, typedWord)) { - suggestions.add(new SuggestedWordInfo(s, - SuggestionStripView.MAX_SUGGESTIONS - i, - SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE - /* autoCommitFirstWordConfidence */)); - } - } - } - mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard()); - mWordComposer.setCursorPositionWithinWord( - typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor)); - mConnection.setComposingRegion( - mLastSelectionStart - numberOfCharsInWordBeforeCursor, - mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor()); - if (suggestions.isEmpty()) { - // We come here if there weren't any suggestion spans on this word. We will try to - // compute suggestions for it instead. - mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING, - SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { - @Override - public void onGetSuggestedWords( - final SuggestedWords suggestedWordsIncludingTypedWord) { - final SuggestedWords suggestedWords; - if (suggestedWordsIncludingTypedWord.size() > 1) { - // We were able to compute new suggestions for this word. - // Remove the typed word, since we don't want to display it in this - // case. The #getSuggestedWordsExcludingTypedWord() method sets - // willAutoCorrect to false. - suggestedWords = suggestedWordsIncludingTypedWord - .getSuggestedWordsExcludingTypedWord(); - } else { - // No saved suggestions, and we were unable to compute any good one - // either. Rather than displaying an empty suggestion strip, we'll - // display the original word alone in the middle. - // Since there is only one word, willAutoCorrect is false. - suggestedWords = suggestedWordsIncludingTypedWord; - } - // We need to pass typedWord because mWordComposer.mTypedWord may - // differ from typedWord. - unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip( - suggestedWords, typedWord); - }}); - } else { - // We found suggestion spans in the word. We'll create the SuggestedWords out of - // them, and make willAutoCorrect false. - final SuggestedWords suggestedWords = new SuggestedWords(suggestions, - true /* typedWordValid */, false /* willAutoCorrect */, - false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, - false /* isPrediction */); - // We need to pass typedWord because mWordComposer.mTypedWord may differ from typedWord. - unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords, typedWord); - } - } - - public void unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip( - final SuggestedWords suggestedWords, final String typedWord) { - // Note that it's very important here that suggestedWords.mWillAutoCorrect is false. - // We never want to auto-correct on a resumed suggestion. Please refer to the three places - // above in restartSuggestionsOnWordTouchedByCursor() where suggestedWords is affected. - // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching - // the text to adapt it. - // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition) - mIsAutoCorrectionIndicatorOn = false; - mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord); - } - - /** - * Check if the cursor is actually at the end of a word. If so, restart suggestions on this - * word, else do nothing. - */ - private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() { - final CharSequence word = - mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent()); - if (null != word) { - final String wordString = word.toString(); - restartSuggestionsOnWordBeforeCursor(wordString); - // TODO: Handle the case where the user manually moves the cursor and then backs up over - // a separator. In that case, the current log unit should not be uncommitted. - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString, - true /* dumpCurrentLogUnit */); - } - } - } - - private void restartSuggestionsOnWordBeforeCursor(final String word) { - mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard()); - final int length = word.length(); - mConnection.deleteSurroundingText(length, 0); - mConnection.setComposingText(word, 1); - mHandler.postUpdateSuggestionStrip(); - } - - /** - * Retry resetting caches in the rich input connection. - * - * When the editor can't be accessed we can't reset the caches, so we schedule a retry. - * This method handles the retry, and re-schedules a new retry if we still can't access. - * We only retry up to 5 times before giving up. - * - * @param tryResumeSuggestions Whether we should resume suggestions or not. - * @param remainingTries How many times we may try again before giving up. - */ - private void retryResetCaches(final boolean tryResumeSuggestions, final int remainingTries) { - if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(mLastSelectionStart, false)) { - if (0 < remainingTries) { - mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1); - return; - } - // If remainingTries is 0, we should stop waiting for new tries, but it's still - // better to load the keyboard (less things will be broken). - } - tryFixLyingCursorPosition(); - mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent()); - if (tryResumeSuggestions) mHandler.postResumeSuggestions(); - } - - private void revertCommit() { - final String previousWord = mLastComposedWord.mPrevWord; - final String originallyTypedWord = mLastComposedWord.mTypedWord; - final String committedWord = mLastComposedWord.mCommittedWord; - final int cancelLength = committedWord.length(); - // We want java chars, not codepoints for the following. - final int separatorLength = mLastComposedWord.mSeparatorString.length(); - // TODO: should we check our saved separator against the actual contents of the text view? - final int deleteLength = cancelLength + separatorLength; - if (DEBUG) { - if (mWordComposer.isComposingWord()) { - throw new RuntimeException("revertCommit, but we are composing a word"); - } - final CharSequence wordBeforeCursor = - mConnection.getTextBeforeCursor(deleteLength, 0) - .subSequence(0, cancelLength); - if (!TextUtils.equals(committedWord, wordBeforeCursor)) { - throw new RuntimeException("revertCommit check failed: we thought we were " - + "reverting \"" + committedWord - + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); - } - } - mConnection.deleteSurroundingText(deleteLength, 0); - if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { - mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord); - } - final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; - if (mSettings.getCurrent().mCurrentLanguageHasSpaces) { - // For languages with spaces, we revert to the typed string, but the cursor is still - // after the separator so we don't resume suggestions. If the user wants to correct - // the word, they have to press backspace again. - mConnection.commitText(stringToCommit, 1); - } 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. - mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard()); - mConnection.setComposingText(stringToCommit, 1); - } - if (mSettings.isInternal()) { - LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString, - Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); - } - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord, - mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString); + @Override + public void showAddToDictionaryHint(final String word) { + if (!hasSuggestionStripView()) { + return; } - // Don't restart suggestion yet. We'll restart if the user deletes the - // separator. - mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; - // We have a separator between the word and the cursor: we should show predictions. - mHandler.postUpdateSuggestionStrip(); + mSuggestionStripView.showAddToDictionaryHint(word); } - // This essentially inserts a space, and that's it. - public void promotePhantomSpace() { + // This will show either an empty suggestion strip (if prediction is enabled) or + // punctuation suggestions (if it's disabled). + @Override + public void setNeutralSuggestionStrip() { final SettingsValues currentSettings = mSettings.getCurrent(); - if (currentSettings.shouldInsertSpacesAutomatically() - && currentSettings.mCurrentLanguageHasSpaces - && !mConnection.textBeforeCursorLooksLikeURL()) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.latinIME_promotePhantomSpace(); - } - sendKeyCodePoint(Constants.CODE_SPACE); - } + final SuggestedWords neutralSuggestions = currentSettings.mBigramPredictionEnabled + ? SuggestedWords.EMPTY : currentSettings.mSpacingAndPunctuations.mSuggestPuncList; + setSuggestedWords(neutralSuggestions, isSuggestionStripVisible()); } // TODO: Make this private @@ -3089,18 +1511,41 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen loadSettings(); if (mKeyboardSwitcher.getMainKeyboardView() != null) { // Reload keyboard because the current language has been changed. - mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent()); + mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent(), + getCurrentAutoCapsState(), getCurrentRecapitalizeState()); + } + } + + /** + * After an input transaction has been executed, some state must be updated. This includes + * the shift state of the keyboard and suggestions. This method looks at the finished + * inputTransaction to find out what is necessary and updates the state accordingly. + * @param inputTransaction The transaction that has been executed. + */ + private void updateStateAfterInputTransaction(final InputTransaction inputTransaction) { + switch (inputTransaction.getRequiredShiftUpdate()) { + case InputTransaction.SHIFT_UPDATE_LATER: + mHandler.postUpdateShiftState(); + break; + case InputTransaction.SHIFT_UPDATE_NOW: + mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); + break; + default: // SHIFT_NO_UPDATE + } + if (inputTransaction.requiresUpdateSuggestions()) { + mHandler.postUpdateSuggestionStrip(); } } private void hapticAndAudioFeedback(final int code, final int repeatCount) { final MainKeyboardView keyboardView = mKeyboardSwitcher.getMainKeyboardView(); - if (keyboardView != null && keyboardView.isInSlidingKeyInput()) { - // No need to feedback while sliding input. + if (keyboardView != null && keyboardView.isInDraggingFinger()) { + // No need to feedback while finger is dragging. return; } if (repeatCount > 0) { - if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) { + if (code == Constants.CODE_DELETE && !mInputLogic.mConnection.canDeleteCharacters()) { // No need to feedback when repeat delete key will have no effect. return; } @@ -3124,7 +1569,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen @Override public void onPressKey(final int primaryCode, final int repeatCount, final boolean isSinglePointer) { - mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer); + mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer, getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); hapticAndAudioFeedback(primaryCode, repeatCount); } @@ -3132,7 +1578,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // press matching call is {@link #onPressKey(int,int,boolean)} above. @Override public void onReleaseKey(final int primaryCode, final boolean withSliding) { - mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding); + mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding, getCurrentAutoCapsState(), + getCurrentRecapitalizeState()); // If accessibility is on, ensure the user receives keyboard state updates. if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { @@ -3147,25 +1594,37 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } + private HardwareEventDecoder getHardwareKeyEventDecoder(final int deviceId) { + final HardwareEventDecoder decoder = mHardwareEventDecoders.get(deviceId); + if (null != decoder) return decoder; + // TODO: create the decoder according to the specification + final HardwareEventDecoder newDecoder = new HardwareKeyboardEventDecoder(deviceId); + mHardwareEventDecoders.put(deviceId, newDecoder); + return newDecoder; + } + // Hooks for hardware keyboard @Override - public boolean onKeyDown(final int keyCode, final KeyEvent event) { - if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) return super.onKeyDown(keyCode, event); - // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if - // it doesn't know what to do with it and leave it to the application. For example, - // hardware key events for adjusting the screen's brightness are passed as is. - if (mEventInterpreter.onHardwareKeyEvent(event)) { - final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); - mCurrentlyPressedHardwareKeys.add(keyIdentifier); + public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) { + if (!ProductionFlag.IS_HARDWARE_KEYBOARD_SUPPORTED) { + return super.onKeyDown(keyCode, keyEvent); + } + final Event event = getHardwareKeyEventDecoder( + keyEvent.getDeviceId()).decodeHardwareKey(keyEvent); + // If the event is not handled by LatinIME, we just pass it to the parent implementation. + // If it's handled, we return true because we did handle it. + if (event.isHandled()) { + mInputLogic.onCodeInput(mSettings.getCurrent(), event, + mKeyboardSwitcher.getKeyboardShiftMode(), mHandler); return true; } - return super.onKeyDown(keyCode, event); + return super.onKeyDown(keyCode, keyEvent); } @Override public boolean onKeyUp(final int keyCode, final KeyEvent event) { final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode(); - if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { + if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) { return true; } return super.onKeyUp(keyCode, event); @@ -3177,7 +1636,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // boolean onKeyMultiple(final int keyCode, final int count, final KeyEvent event); // receive ringer mode change and network state change. - private BroadcastReceiver mReceiver = new BroadcastReceiver() { + private BroadcastReceiver mConnectivityAndRingerModeChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); @@ -3190,17 +1649,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen }; private void launchSettings() { - handleClose(); + mInputLogic.commitTyped(mSettings.getCurrent(), LastComposedWord.NOT_A_SEPARATOR); + requestHideSelf(0); + final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView(); + if (mainKeyboardView != null) { + mainKeyboardView.closing(); + } launchSubActivity(SettingsActivity.class); } - public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) { - // Put the text in the attached EditText into a safe, saved state before switching to a - // new activity that will also use the soft keyboard. - commitTyped(LastComposedWord.NOT_A_SEPARATOR); - launchSubActivity(activityClass); - } - private void launchSubActivity(final Class<? extends Activity> activityClass) { Intent intent = new Intent(); intent.setClass(LatinIME.this, activityClass); @@ -3215,9 +1672,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final CharSequence[] items = new CharSequence[] { // TODO: Should use new string "Select active input modes". getString(R.string.language_selection_title), - getString(ApplicationUtils.getAcitivityTitleResId(this, SettingsActivity.class)), + getString(ApplicationUtils.getActivityTitleResId(this, SettingsActivity.class)), }; - final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { + final OnClickListener listener = new OnClickListener() { @Override public void onClick(DialogInterface di, int position) { di.dismiss(); @@ -3226,8 +1683,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final Intent intent = IntentUtils.getInputLanguageSelectionIntent( mRichImm.getInputMethodIdOfThisIme(), Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED - | Intent.FLAG_ACTIVITY_CLEAR_TOP); + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); break; case 1: @@ -3236,21 +1693,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen } } }; - final AlertDialog.Builder builder = new AlertDialog.Builder(this) - .setItems(items, listener) - .setTitle(title); - showOptionDialog(builder.create()); + final AlertDialog.Builder builder = new AlertDialog.Builder( + DialogUtils.getPlatformDialogThemeContext(this)); + builder.setItems(items, listener).setTitle(title); + final AlertDialog dialog = builder.create(); + dialog.setCancelable(true /* cancelable */); + dialog.setCanceledOnTouchOutside(true /* cancelable */); + showOptionDialog(dialog); } - public void showOptionDialog(final AlertDialog dialog) { + // TODO: Move this method out of {@link LatinIME}. + private void showOptionDialog(final AlertDialog dialog) { final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken(); if (windowToken == null) { return; } - dialog.setCancelable(true); - dialog.setCanceledOnTouchOutside(true); - final Window window = dialog.getWindow(); final WindowManager.LayoutParams lp = window.getAttributes(); lp.token = windowToken; @@ -3264,31 +1722,48 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen // TODO: can this be removed somehow without breaking the tests? @UsedForTesting - /* package for test */ String getFirstSuggestedWord() { - return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null; + /* package for test */ SuggestedWords getSuggestedWordsForTest() { + // You may not use this method for anything else than debug + return DEBUG ? mInputLogic.mSuggestedWords : null; } // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME. @UsedForTesting - /* package for test */ boolean isCurrentlyWaitingForMainDictionary() { - return mSuggest.isCurrentlyWaitingForMainDictionary(); + /* package for test */ void waitForLoadingDictionaries(final long timeout, final TimeUnit unit) + throws InterruptedException { + mInputLogic.mSuggest.mDictionaryFacilitator.waitForLoadingDictionariesForTesting( + timeout, unit); } - // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME. + // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly. @UsedForTesting - /* package for test */ boolean hasMainDictionary() { - return mSuggest.hasMainDictionary(); + /* package for test */ void replaceDictionariesForTest(final Locale locale) { + final SettingsValues settingsValues = mSettings.getCurrent(); + mInputLogic.mSuggest.mDictionaryFacilitator.resetDictionaries(this, locale, + settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts, + false /* forceReloadMainDictionary */, this /* listener */); } - // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly. + // DO NOT USE THIS for any other purpose than testing. @UsedForTesting - /* package for test */ void replaceMainDictionaryForTest(final Locale locale) { - mSuggest.resetMainDict(this, locale, null); + /* package for test */ void clearPersonalizedDictionariesForTest() { + mInputLogic.mSuggest.mDictionaryFacilitator.clearUserHistoryDictionary(); + mInputLogic.mSuggest.mDictionaryFacilitator.clearPersonalizationDictionary(); + } + + public void dumpDictionaryForDebug(final String dictName) { + final DictionaryFacilitatorForSuggest dictionaryFacilitator = + mInputLogic.mSuggest.mDictionaryFacilitator; + if (dictionaryFacilitator.getLocale() == null) { + resetSuggest(); + } + mInputLogic.mSuggest.mDictionaryFacilitator.dumpDictionaryForDebug(dictName); } public void debugDumpStateAndCrashWithException(final String context) { - final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString()); - s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes) + final SettingsValues settingsValues = mSettings.getCurrent(); + final StringBuilder s = new StringBuilder(settingsValues.toString()); + s.append("\nAttributes : ").append(settingsValues.mInputAttributes) .append("\nContext : ").append(context); throw new RuntimeException(s.toString()); } @@ -3299,17 +1774,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen final Printer p = new PrintWriterPrinter(fout); p.println("LatinIME state :"); + p.println(" VersionCode = " + ApplicationUtils.getVersionCode(this)); + p.println(" VersionName = " + ApplicationUtils.getVersionName(this)); final Keyboard keyboard = mKeyboardSwitcher.getKeyboard(); final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1; p.println(" Keyboard mode = " + keyboardMode); final SettingsValues settingsValues = mSettings.getCurrent(); - p.println(" mIsSuggestionsSuggestionsRequested = " - + settingsValues.isSuggestionsRequested(mDisplayOrientation)); - p.println(" mCorrectionEnabled=" + settingsValues.mCorrectionEnabled); - p.println(" isComposingWord=" + mWordComposer.isComposingWord()); - p.println(" mSoundOn=" + settingsValues.mSoundOn); - p.println(" mVibrateOn=" + settingsValues.mVibrateOn); - p.println(" mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn); - p.println(" inputAttributes=" + settingsValues.mInputAttributes); + p.println(settingsValues.dump()); + // TODO: Dump all settings values } } diff --git a/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java new file mode 100644 index 000000000..4911bcdf6 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/PunctuationSuggestions.java @@ -0,0 +1,116 @@ +/* + * 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 com.android.inputmethod.keyboard.internal.KeySpecParser; +import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * The extended {@link SuggestedWords} class to represent punctuation suggestions. + * + * Each punctuation specification string is the key specification that can be parsed by + * {@link KeySpecParser}. + */ +public final class PunctuationSuggestions extends SuggestedWords { + private PunctuationSuggestions(final ArrayList<SuggestedWordInfo> punctuationsList) { + super(punctuationsList, + null /* rawSuggestions */, + false /* typedWordValid */, + false /* hasAutoCorrectionCandidate */, + false /* isObsoleteSuggestions */, + false /* isPrediction */); + } + + /** + * Create new instance of {@link PunctuationSuggestions} from the array of punctuation key + * specifications. + * + * @param punctuationSpecs The array of punctuation key specifications. + * @return The {@link PunctuationSuggestions} object. + */ + public static PunctuationSuggestions newPunctuationSuggestions( + final String[] punctuationSpecs) { + final ArrayList<SuggestedWordInfo> puncuationsList = CollectionUtils.newArrayList(); + for (final String puncSpec : punctuationSpecs) { + puncuationsList.add(newHardCodedWordInfo(puncSpec)); + } + return new PunctuationSuggestions(puncuationsList); + } + + /** + * {@inheritDoc} + * Note that {@link super#getWord(int)} returns a punctuation key specification text. + * The suggested punctuation should be gotten by parsing the key specification. + */ + @Override + public String getWord(final int index) { + final String keySpec = super.getWord(index); + final int code = KeySpecParser.getCode(keySpec); + return (code == Constants.CODE_OUTPUT_TEXT) + ? KeySpecParser.getOutputText(keySpec) + : StringUtils.newSingleCodePointString(code); + } + + /** + * {@inheritDoc} + * Note that {@link super#getWord(int)} returns a punctuation key specification text. + * The displayed text should be gotten by parsing the key specification. + */ + @Override + public String getLabel(final int index) { + final String keySpec = super.getWord(index); + return KeySpecParser.getLabel(keySpec); + } + + /** + * {@inheritDoc} + * Note that {@link #getWord(int)} returns a suggested punctuation. We should create a + * {@link SuggestedWordInfo} object that represents a hard coded word. + */ + @Override + public SuggestedWordInfo getInfo(final int index) { + return newHardCodedWordInfo(getWord(index)); + } + + /** + * The predicator to tell whether this object represents punctuation suggestions. + * @return true if this object represents punctuation suggestions. + */ + @Override + public boolean isPunctuationSuggestions() { + return true; + } + + @Override + public String toString() { + return "PunctuationSuggestions: " + + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray()); + } + + private static SuggestedWordInfo newHardCodedWordInfo(final String keySpec) { + return new SuggestedWordInfo(keySpec, SuggestedWordInfo.MAX_SCORE, + SuggestedWordInfo.KIND_HARDCODED, + Dictionary.DICTIONARY_HARDCODED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); + } +} diff --git a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java index 68505ce38..9f61d6c37 100644 --- a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java @@ -51,20 +51,21 @@ public final class ReadOnlyBinaryDictionary extends Dictionary { @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final float[] inOutLanguageWeight) { return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, 0 /* sessionId */); + additionalFeaturesOptions, 0 /* sessionId */, inOutLanguageWeight); } @Override public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, - final int sessionId) { + final int sessionId, final float[] inOutLanguageWeight) { if (mLock.readLock().tryLock()) { try { return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo, - blockOffensiveWords, additionalFeaturesOptions); + blockOffensiveWords, additionalFeaturesOptions, inOutLanguageWeight); } 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 673d1b4c2..606bb775e 100644 --- a/java/src/com/android/inputmethod/latin/RichInputConnection.java +++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java @@ -27,7 +27,7 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import com.android.inputmethod.latin.define.ProductionFlag; -import com.android.inputmethod.latin.settings.SettingsValues; +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.SpannableStringUtils; @@ -35,7 +35,7 @@ import com.android.inputmethod.latin.utils.StringUtils; import com.android.inputmethod.latin.utils.TextRange; import com.android.inputmethod.research.ResearchLogger; -import java.util.Locale; +import java.util.Arrays; import java.util.regex.Pattern; /** @@ -57,14 +57,19 @@ public final class RichInputConnection { private static final int INVALID_CURSOR_POSITION = -1; /** - * This variable contains an expected value for the cursor position. This is where the - * cursor may end up after all the keyboard-triggered updates have passed. We keep this to - * compare it to the actual cursor position to guess whether the move was caused by a - * keyboard command or not. - * It's not really the cursor position: the cursor may not be there yet, and it's also expected - * there be cases where it never actually comes to be there. + * This variable contains an expected value for the selection start position. This is where the + * cursor or selection start may end up after all the keyboard-triggered updates have passed. We + * keep this to compare it to the actual selection start to guess whether the move was caused by + * a keyboard command or not. + * It's not really the selection start position: the selection start may not be there yet, and + * in some cases, it may never arrive there. */ - private int mExpectedCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points + private int mExpectedSelStart = INVALID_CURSOR_POSITION; // in chars, not code points + /** + * The expected selection end. Only differs from mExpectedSelStart if a non-empty selection is + * expected. The same caveats as mExpectedSelStart apply. + */ + private int mExpectedSelEnd = INVALID_CURSOR_POSITION; // in chars, not code points /** * This contains the committed text immediately preceding the cursor and the composing * text if any. It is refreshed when the cursor moves by calling upon the TextView. @@ -93,7 +98,7 @@ public final class RichInputConnection { final ExtractedText et = mIC.getExtractedText(r, 0); final CharSequence beforeCursor = getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); - final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText) + final StringBuilder internal = new StringBuilder(mCommittedTextBeforeComposingText) .append(mComposingText); if (null == et || null == beforeCursor) return; final int actualLength = Math.min(beforeCursor.length(), internal.length()); @@ -103,16 +108,16 @@ public final class RichInputConnection { final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString() : beforeCursor.subSequence(beforeCursor.length() - actualLength, beforeCursor.length()).toString(); - if (et.selectionStart != mExpectedCursorPosition + if (et.selectionStart != mExpectedSelStart || !(reference.equals(internal.toString()))) { - final String context = "Expected cursor position = " + mExpectedCursorPosition - + "\nActual cursor position = " + et.selectionStart + final String context = "Expected selection start = " + mExpectedSelStart + + "\nActual selection start = " + et.selectionStart + "\nExpected text = " + internal.length() + " " + internal + "\nActual text = " + reference.length() + " " + reference; ((LatinIME)mParent).debugDumpStateAndCrashWithException(context); } else { Log.e(TAG, DebugLogUtils.getStackTrace(2)); - Log.e(TAG, "Exp <> Actual : " + mExpectedCursorPosition + " <> " + et.selectionStart); + Log.e(TAG, "Exp <> Actual : " + mExpectedSelStart + " <> " + et.selectionStart); } } @@ -150,16 +155,38 @@ public final class RichInputConnection { * data, so we empty the cache and note that we don't know the new cursor position, and we * return false so that the caller knows about this and can retry later. * - * @param newCursorPosition The new position of the cursor, as received from the system. - * @param shouldFinishComposition Whether we should finish the composition in progress. + * @param newSelStart the new position of the selection start, as received from the system. + * @param newSelEnd the new position of the selection end, as received from the system. + * @param shouldFinishComposition whether we should finish the composition in progress. * @return true if we were able to connect to the editor successfully, false otherwise. When * this method returns false, the caches could not be correctly refreshed so they were only * reset: the caller should try again later to return to normal operation. */ - public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newCursorPosition, - final boolean shouldFinishComposition) { - mExpectedCursorPosition = newCursorPosition; + public boolean resetCachesUponCursorMoveAndReturnSuccess(final int newSelStart, + final int newSelEnd, final boolean shouldFinishComposition) { + mExpectedSelStart = newSelStart; + mExpectedSelEnd = newSelEnd; mComposingText.setLength(0); + final boolean didReloadTextSuccessfully = reloadTextCache(); + if (!didReloadTextSuccessfully) { + Log.d(TAG, "Will try to retrieve text later."); + return false; + } + if (null != mIC && shouldFinishComposition) { + mIC.finishComposingText(); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.richInputConnection_finishComposingText(); + } + } + return true; + } + + /** + * Reload the cached text from the InputConnection. + * + * @return true if successful + */ + private boolean reloadTextCache() { mCommittedTextBeforeComposingText.setLength(0); mIC = mParent.getCurrentInputConnection(); // Call upon the inputconnection directly since our own method is using the cache, and @@ -169,27 +196,12 @@ public final class RichInputConnection { if (null == textBeforeCursor) { // For some reason the app thinks we are not connected to it. This looks like a // framework bug... Fall back to ground state and return false. - mExpectedCursorPosition = INVALID_CURSOR_POSITION; - Log.e(TAG, "Unable to connect to the editor to retrieve text... will retry later"); + mExpectedSelStart = INVALID_CURSOR_POSITION; + mExpectedSelEnd = INVALID_CURSOR_POSITION; + Log.e(TAG, "Unable to connect to the editor to retrieve text."); return false; } mCommittedTextBeforeComposingText.append(textBeforeCursor); - final int lengthOfTextBeforeCursor = textBeforeCursor.length(); - if (lengthOfTextBeforeCursor > newCursorPosition - || (lengthOfTextBeforeCursor < Constants.EDITOR_CONTENTS_CACHE_SIZE - && newCursorPosition < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { - // newCursorPosition may be lying -- when rotating the device (probably a framework - // bug). If we have less chars than we asked for, then we know how many chars we have, - // and if we got more than newCursorPosition says, then we know it was lying. In both - // cases the length is more reliable - mExpectedCursorPosition = lengthOfTextBeforeCursor; - } - if (null != mIC && shouldFinishComposition) { - mIC.finishComposingText(); - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - ResearchLogger.richInputConnection_finishComposingText(); - } - } return true; } @@ -204,6 +216,9 @@ public final class RichInputConnection { public void finishComposingText() { if (DEBUG_BATCH_NESTING) checkBatchEdit(); if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + // TODO: this is not correct! The cursor is not necessarily after the composing text. + // In the practice right now this is only called when input ends so it will be reset so + // it works, but it's wrong and should be fixed. mCommittedTextBeforeComposingText.append(mComposingText); mComposingText.setLength(0); if (null != mIC) { @@ -218,7 +233,11 @@ public final class RichInputConnection { if (DEBUG_BATCH_NESTING) checkBatchEdit(); if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); mCommittedTextBeforeComposingText.append(text); - mExpectedCursorPosition += text.length() - mComposingText.length(); + // TODO: the following is exceedingly error-prone. Right now when the cursor is in the + // middle of the composing word mComposingText only holds the part of the composing text + // that is before the cursor, so this actually works, but it's terribly confusing. Fix this. + mExpectedSelStart += text.length() - mComposingText.length(); + mExpectedSelEnd = mExpectedSelStart; mComposingText.setLength(0); if (null != mIC) { mIC.commitText(text, i); @@ -226,12 +245,11 @@ public final class RichInputConnection { } public CharSequence getSelectedText(final int flags) { - if (null == mIC) return null; - return mIC.getSelectedText(flags); + return (null == mIC) ? null : mIC.getSelectedText(flags); } public boolean canDeleteCharacters() { - return mExpectedCursorPosition > 0; + return mExpectedSelStart > 0; } /** @@ -245,12 +263,12 @@ public final class RichInputConnection { * American English, it's just the most common set of rules for English). * * @param inputType a mask of the caps modes to test for. - * @param settingsValues the values of the settings to use for locale and separators. + * @param spacingAndPunctuations the values of the settings to use for locale and separators. * @param hasSpaceBefore if we should consider there should be a space after the string. * @return the caps modes that should be on as a set of bits */ - public int getCursorCapsMode(final int inputType, final SettingsValues settingsValues, - final boolean hasSpaceBefore) { + public int getCursorCapsMode(final int inputType, + final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) { mIC = mParent.getCurrentInputConnection(); if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF; if (!TextUtils.isEmpty(mComposingText)) { @@ -268,23 +286,22 @@ public final class RichInputConnection { // heavy pressing of delete, for example DEFAULT_TEXT_CACHE_SIZE - 5 times or so. // getCapsMode should be updated to be able to return a "not enough info" result so that // we can get more context only when needed. - if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedCursorPosition) { - final CharSequence textBeforeCursor = getTextBeforeCursor( - Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); - if (!TextUtils.isEmpty(textBeforeCursor)) { - mCommittedTextBeforeComposingText.append(textBeforeCursor); + if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedSelStart) { + if (!reloadTextCache()) { + Log.w(TAG, "Unable to connect to the editor. " + + "Setting caps mode without knowing text."); } } // This never calls InputConnection#getCapsMode - in fact, it's a static method that // never blocks or initiates IPC. return CapsModeUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, - settingsValues, hasSpaceBefore); + spacingAndPunctuations, hasSpaceBefore); } public int getCodePointBeforeCursor() { - if (mCommittedTextBeforeComposingText.length() < 1) return Constants.NOT_A_CODE; - return Character.codePointBefore(mCommittedTextBeforeComposingText, - mCommittedTextBeforeComposingText.length()); + final int length = mCommittedTextBeforeComposingText.length(); + if (length < 1) return Constants.NOT_A_CODE; + return Character.codePointBefore(mCommittedTextBeforeComposingText, length); } public CharSequence getTextBeforeCursor(final int n, final int flags) { @@ -295,8 +312,8 @@ public final class RichInputConnection { // However, if we don't have an expected cursor position, then we should always // go fetch the cache again (as it happens, INVALID_CURSOR_POSITION < 0, so we need to // test for this explicitly) - if (INVALID_CURSOR_POSITION != mExpectedCursorPosition - && (cachedLength >= n || cachedLength >= mExpectedCursorPosition)) { + if (INVALID_CURSOR_POSITION != mExpectedSelStart + && (cachedLength >= n || cachedLength >= mExpectedSelStart)) { final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText); // We call #toString() here to create a temporary object. // In some situations, this method is called on a worker thread, and it's possible @@ -312,20 +329,19 @@ public final class RichInputConnection { return s; } mIC = mParent.getCurrentInputConnection(); - if (null != mIC) { - return mIC.getTextBeforeCursor(n, flags); - } - return null; + return (null == mIC) ? null : mIC.getTextBeforeCursor(n, flags); } public CharSequence getTextAfterCursor(final int n, final int flags) { mIC = mParent.getCurrentInputConnection(); - if (null != mIC) return mIC.getTextAfterCursor(n, flags); - return null; + return (null == mIC) ? null : mIC.getTextAfterCursor(n, flags); } public void deleteSurroundingText(final int beforeLength, final int afterLength) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); + // TODO: the following is incorrect if the cursor is not immediately after the composition. + // Right now we never come here in this case because we reset the composing state before we + // come here in this case, but we need to fix this. final int remainingChars = mComposingText.length() - beforeLength; if (remainingChars >= 0) { mComposingText.setLength(remainingChars); @@ -336,10 +352,14 @@ public final class RichInputConnection { + remainingChars, 0); mCommittedTextBeforeComposingText.setLength(len); } - if (mExpectedCursorPosition > beforeLength) { - mExpectedCursorPosition -= beforeLength; + if (mExpectedSelStart > beforeLength) { + mExpectedSelStart -= beforeLength; + mExpectedSelEnd -= beforeLength; } else { - mExpectedCursorPosition = 0; + // There are fewer characters before the cursor in the buffer than we are being asked to + // delete. Only delete what is there, and update the end with the amount deleted. + mExpectedSelEnd -= mExpectedSelStart; + mExpectedSelStart = 0; } if (null != mIC) { mIC.deleteSurroundingText(beforeLength, afterLength); @@ -373,7 +393,8 @@ public final class RichInputConnection { switch (keyEvent.getKeyCode()) { case KeyEvent.KEYCODE_ENTER: mCommittedTextBeforeComposingText.append("\n"); - mExpectedCursorPosition += 1; + mExpectedSelStart += 1; + mExpectedSelEnd = mExpectedSelStart; break; case KeyEvent.KEYCODE_DEL: if (0 == mComposingText.length()) { @@ -385,18 +406,24 @@ public final class RichInputConnection { } else { mComposingText.delete(mComposingText.length() - 1, mComposingText.length()); } - if (mExpectedCursorPosition > 0) mExpectedCursorPosition -= 1; + if (mExpectedSelStart > 0 && mExpectedSelStart == mExpectedSelEnd) { + // TODO: Handle surrogate pairs. + mExpectedSelStart -= 1; + } + mExpectedSelEnd = mExpectedSelStart; break; case KeyEvent.KEYCODE_UNKNOWN: if (null != keyEvent.getCharacters()) { mCommittedTextBeforeComposingText.append(keyEvent.getCharacters()); - mExpectedCursorPosition += keyEvent.getCharacters().length(); + mExpectedSelStart += keyEvent.getCharacters().length(); + mExpectedSelEnd = mExpectedSelStart; } break; default: - final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1); + final String text = StringUtils.newSingleCodePointString(keyEvent.getUnicodeChar()); mCommittedTextBeforeComposingText.append(text); - mExpectedCursorPosition += text.length(); + mExpectedSelStart += text.length(); + mExpectedSelEnd = mExpectedSelStart; break; } } @@ -415,8 +442,12 @@ public final class RichInputConnection { getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE + (end - start), 0); mCommittedTextBeforeComposingText.setLength(0); if (!TextUtils.isEmpty(textBeforeCursor)) { + // The cursor is not necessarily at the end of the composing text, but we have its + // position in mExpectedSelStart and mExpectedSelEnd. In this case we want the start + // of the text, so we should use mExpectedSelStart. In other words, the composing + // text starts (mExpectedSelStart - start) characters before the end of textBeforeCursor final int indexOfStartOfComposingText = - Math.max(textBeforeCursor.length() - (end - start), 0); + Math.max(textBeforeCursor.length() - (mExpectedSelStart - start), 0); mComposingText.append(textBeforeCursor.subSequence(indexOfStartOfComposingText, textBeforeCursor.length())); mCommittedTextBeforeComposingText.append( @@ -430,10 +461,12 @@ public final class RichInputConnection { public void setComposingText(final CharSequence text, final int newCursorPosition) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); - mExpectedCursorPosition += text.length() - mComposingText.length(); + mExpectedSelStart += text.length() - mComposingText.length(); + mExpectedSelEnd = mExpectedSelStart; mComposingText.setLength(0); mComposingText.append(text); - // TODO: support values of i != 1. At this time, this is never called with i != 1. + // TODO: support values of newCursorPosition != 1. At this time, this is never called with + // newCursorPosition != 1. if (null != mIC) { mIC.setComposingText(text, newCursorPosition); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { @@ -443,19 +476,35 @@ public final class RichInputConnection { if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); } - public void setSelection(final int start, final int end) { + /** + * Set the selection of the text editor. + * + * Calls through to {@link InputConnection#setSelection(int, int)}. + * + * @param start the character index where the selection should start. + * @param end the character index where the selection should end. + * @return Returns true on success, false on failure: either the input connection is no longer + * valid when setting the selection or when retrieving the text cache at that point, or + * invalid arguments were passed. + */ + public boolean setSelection(final int start, final int end) { if (DEBUG_BATCH_NESTING) checkBatchEdit(); if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug(); + if (start < 0 || end < 0) { + return false; + } + mExpectedSelStart = start; + mExpectedSelEnd = end; if (null != mIC) { - mIC.setSelection(start, end); + final boolean isIcValid = mIC.setSelection(start, end); + if (!isIcValid) { + return false; + } if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.richInputConnection_setSelection(start, end); } } - mExpectedCursorPosition = start; - mCommittedTextBeforeComposingText.setLength(0); - mCommittedTextBeforeComposingText.append( - getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0)); + return reloadTextCache(); } public void commitCorrection(final CorrectionInfo correctionInfo) { @@ -476,7 +525,8 @@ public final class RichInputConnection { // text should never be null, but just in case, it's better to insert nothing than to crash if (null == text) text = ""; mCommittedTextBeforeComposingText.append(text); - mExpectedCursorPosition += text.length() - mComposingText.length(); + mExpectedSelStart += text.length() - mComposingText.length(); + mExpectedSelEnd = mExpectedSelStart; mComposingText.setLength(0); if (null != mIC) { mIC.commitCompletion(completionInfo); @@ -488,7 +538,8 @@ public final class RichInputConnection { } @SuppressWarnings("unused") - public String getNthPreviousWord(final String sentenceSeperators, final int n) { + public String getNthPreviousWord(final SpacingAndPunctuations spacingAndPunctuations, + final int n) { mIC = mParent.getCurrentInputConnection(); if (null == mIC) return null; final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0); @@ -496,6 +547,9 @@ public final class RichInputConnection { final int checkLength = LOOKBACK_CHARACTER_NUM - 1; final String reference = prev.length() <= checkLength ? prev.toString() : prev.subSequence(prev.length() - checkLength, prev.length()).toString(); + // TODO: right now the following works because mComposingText holds the part of the + // composing text that is before the cursor, but this is very confusing. We should + // fix it. final StringBuilder internal = new StringBuilder() .append(mCommittedTextBeforeComposingText).append(mComposingText); if (internal.length() > checkLength) { @@ -507,11 +561,11 @@ public final class RichInputConnection { } } } - return getNthPreviousWord(prev, sentenceSeperators, n); + return getNthPreviousWord(prev, spacingAndPunctuations, n); } - private static boolean isSeparator(int code, String sep) { - return sep.indexOf(code) != -1; + private static boolean isSeparator(final int code, final int[] sortedSeparators) { + return Arrays.binarySearch(sortedSeparators, code) >= 0; } // Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor, @@ -531,7 +585,7 @@ public final class RichInputConnection { // (n = 2) "abc |" -> null // (n = 2) "abc. def|" -> null public static String getNthPreviousWord(final CharSequence prev, - final String sentenceSeperators, final int n) { + final SpacingAndPunctuations spacingAndPunctuations, final int n) { if (prev == null) return null; final String[] w = spaceRegex.split(prev); @@ -543,35 +597,36 @@ public final class RichInputConnection { // If ends in a separator, return null final char lastChar = nthPrevWord.charAt(length - 1); - if (sentenceSeperators.contains(String.valueOf(lastChar))) return null; + if (spacingAndPunctuations.isWordSeparator(lastChar) + || spacingAndPunctuations.isWordConnector(lastChar)) return null; return nthPrevWord; } /** - * @param separators characters which may separate words + * @param sortedSeparators a sorted array of code points which may separate words * @return the word that surrounds the cursor, including up to one trailing * separator. For example, if the field contains "he|llo world", where | * represents the cursor, then "hello " will be returned. */ - public CharSequence getWordAtCursor(String separators) { + public CharSequence getWordAtCursor(final int[] sortedSeparators) { // getWordRangeAtCursor returns null if the connection is null - TextRange r = getWordRangeAtCursor(separators, 0); + final TextRange r = getWordRangeAtCursor(sortedSeparators, 0); return (r == null) ? null : r.mWord; } /** * Returns the text surrounding the cursor. * - * @param sep a string of characters that split words. + * @param sortedSeparators a sorted array of code points that split words. * @param additionalPrecedingWordsCount the number of words before the current word that should * be included in the returned range * @return a range containing the text surrounding the cursor */ - public TextRange getWordRangeAtCursor(final String sep, + public TextRange getWordRangeAtCursor(final int[] sortedSeparators, final int additionalPrecedingWordsCount) { mIC = mParent.getCurrentInputConnection(); - if (mIC == null || sep == null) { + if (mIC == null) { return null; } final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, @@ -590,7 +645,7 @@ public final class RichInputConnection { while (true) { // see comments below for why this is guaranteed to halt while (startIndexInBefore > 0) { final int codePoint = Character.codePointBefore(before, startIndexInBefore); - if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) { + if (isStoppingAtWhitespace == isSeparator(codePoint, sortedSeparators)) { break; // inner loop } --startIndexInBefore; @@ -611,7 +666,7 @@ public final class RichInputConnection { int endIndexInAfter = -1; while (++endIndexInAfter < after.length()) { final int codePoint = Character.codePointAt(after, endIndexInAfter); - if (isSeparator(codePoint, sep)) { + if (isSeparator(codePoint, sortedSeparators)) { break; } if (Character.isSupplementaryCodePoint(codePoint)) { @@ -619,27 +674,50 @@ public final class RichInputConnection { } } + final boolean hasUrlSpans = + SpannableStringUtils.hasUrlSpans(before, startIndexInBefore, before.length()) + || SpannableStringUtils.hasUrlSpans(after, 0, endIndexInAfter); // We don't use TextUtils#concat because it copies all spans without respect to their // nature. If the text includes a PARAGRAPH span and it has been split, then // TextUtils#concat will crash when it tries to concat both sides of it. return new TextRange( SpannableStringUtils.concatWithNonParagraphSuggestionSpansOnly(before, after), - startIndexInBefore, before.length() + endIndexInAfter, before.length()); + startIndexInBefore, before.length() + endIndexInAfter, before.length(), + hasUrlSpans); } - public boolean isCursorTouchingWord(final SettingsValues settingsValues) { - final int codePointBeforeCursor = getCodePointBeforeCursor(); - if (Constants.NOT_A_CODE != codePointBeforeCursor - && !settingsValues.isWordSeparator(codePointBeforeCursor) - && !settingsValues.isWordConnector(codePointBeforeCursor)) { + public boolean isCursorTouchingWord(final SpacingAndPunctuations spacingAndPunctuations) { + if (isCursorFollowedByWordCharacter(spacingAndPunctuations)) { + // If what's after the cursor is a word character, then we're touching a word. return true; } + final String textBeforeCursor = mCommittedTextBeforeComposingText.toString(); + int indexOfCodePointInJavaChars = textBeforeCursor.length(); + int consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE + : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); + // Search for the first non word-connector char + if (spacingAndPunctuations.isWordConnector(consideredCodePoint)) { + indexOfCodePointInJavaChars -= Character.charCount(consideredCodePoint); + consideredCodePoint = 0 == indexOfCodePointInJavaChars ? Constants.NOT_A_CODE + : textBeforeCursor.codePointBefore(indexOfCodePointInJavaChars); + } + return !(Constants.NOT_A_CODE == consideredCodePoint + || spacingAndPunctuations.isWordSeparator(consideredCodePoint) + || spacingAndPunctuations.isWordConnector(consideredCodePoint)); + } + + public boolean isCursorFollowedByWordCharacter( + final SpacingAndPunctuations spacingAndPunctuations) { final CharSequence after = getTextAfterCursor(1, 0); - if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0)) - && !settingsValues.isWordConnector(after.charAt(0))) { - return true; + if (TextUtils.isEmpty(after)) { + return false; + } + final int codePointAfterCursor = Character.codePointAt(after, 0); + if (spacingAndPunctuations.isWordSeparator(codePointAfterCursor) + || spacingAndPunctuations.isWordConnector(codePointAfterCursor)) { + return false; } - return false; + return true; } public void removeTrailingSpace() { @@ -655,45 +733,6 @@ public final class RichInputConnection { return TextUtils.equals(text, beforeText); } - /* (non-javadoc) - * Returns the word before the cursor if the cursor is at the end of a word, null otherwise - */ - public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) { - // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace, - // separator or end of line/text) - // Example: "test|"<EOL> "te|st" get rejected here - final CharSequence textAfterCursor = getTextAfterCursor(1, 0); - if (!TextUtils.isEmpty(textAfterCursor) - && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null; - - // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe) - // Example: " -|" gets rejected here but "e-|" and "e|" are okay - CharSequence word = getWordAtCursor(settings.mWordSeparators); - // We don't suggest on leading single quotes, so we have to remove them from the word if - // it starts with single quotes. - while (!TextUtils.isEmpty(word) && Constants.CODE_SINGLE_QUOTE == word.charAt(0)) { - word = word.subSequence(1, word.length()); - } - if (TextUtils.isEmpty(word)) return null; - // Find the last code point of the string - final int lastCodePoint = Character.codePointBefore(word, word.length()); - // If for some reason the text field contains non-unicode binary data, or if the - // charsequence is exactly one char long and the contents is a low surrogate, return null. - if (!Character.isDefined(lastCodePoint)) return null; - // Bail out if the cursor is not at the end of a word (cursor must be preceded by - // non-whitespace, non-separator, non-start-of-text) - // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here. - if (settings.isWordSeparator(lastCodePoint)) return null; - final char firstChar = word.charAt(0); // we just tested that word is not empty - if (word.length() == 1 && !Character.isLetter(firstChar)) return null; - - // We don't restart suggestion if the first character is not a letter, because we don't - // start composing when the first character is not a letter. - if (!Character.isLetter(firstChar)) return null; - - return word; - } - public boolean revertDoubleSpacePeriod() { if (DEBUG_BATCH_NESTING) checkBatchEdit(); // Here we test whether we indeed have a period and a space before us. This should not @@ -758,20 +797,30 @@ public final class RichInputConnection { * this update and not the ones in-between. This is almost impossible to achieve even trying * very very hard. * - * @param oldSelStart The value of the old cursor position in the update. - * @param newSelStart The value of the new cursor position in the update. + * @param oldSelStart The value of the old selection in the update. + * @param newSelStart The value of the new selection in the update. + * @param oldSelEnd The value of the old selection end in the update. + * @param newSelEnd The value of the new selection end in the update. * @return whether this is a belated expected update or not. */ - public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart) { - // If this is an update that arrives at our expected position, it's a belated update. - if (newSelStart == mExpectedCursorPosition) return true; - // If this is an update that moves the cursor from our expected position, it must be - // an explicit move. - if (oldSelStart == mExpectedCursorPosition) return false; - // The following returns true if newSelStart is between oldSelStart and - // mCurrentCursorPosition. We assume that if the updated position is between the old - // position and the expected position, then it must be a belated update. - return (newSelStart - oldSelStart) * (mExpectedCursorPosition - newSelStart) >= 0; + public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart, + final int oldSelEnd, final int newSelEnd) { + // This update is "belated" if we are expecting it. That is, mExpectedSelStart and + // mExpectedSelEnd match the new values that the TextView is updating TO. + if (mExpectedSelStart == newSelStart && mExpectedSelEnd == newSelEnd) return true; + // This update is not belated if mExpectedSelStart and mExpectedSelEnd match the old + // values, and one of newSelStart or newSelEnd is updated to a different value. In this + // case, it is likely that something other than the IME has moved the selection endpoint + // to the new value. + if (mExpectedSelStart == oldSelStart && mExpectedSelEnd == oldSelEnd + && (oldSelStart != newSelStart || oldSelEnd != newSelEnd)) return false; + // If neither of the above two cases hold, then the system may be having trouble keeping up + // with updates. If 1) the selection is a cursor, 2) newSelStart is between oldSelStart + // and mExpectedSelStart, and 3) newSelEnd is between oldSelEnd and mExpectedSelEnd, then + // assume a belated update. + return (newSelStart == newSelEnd) + && (newSelStart - oldSelStart) * (mExpectedSelStart - newSelStart) >= 0 + && (newSelEnd - oldSelEnd) * (mExpectedSelEnd - newSelEnd) >= 0; } /** @@ -784,4 +833,65 @@ public final class RichInputConnection { public boolean textBeforeCursorLooksLikeURL() { return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); } + + /** + * Looks at the text just before the cursor to find out if we are inside a double quote. + * + * As with #textBeforeCursorLooksLikeURL, this is dependent on how much text we have cached. + * However this won't be a concrete problem in most situations, as the cache is almost always + * long enough for this use. + */ + public boolean isInsideDoubleQuoteOrAfterDigit() { + return StringUtils.isInsideDoubleQuoteOrAfterDigit(mCommittedTextBeforeComposingText); + } + + /** + * 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 + * 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() { + final CharSequence textBeforeCursor = getTextBeforeCursor( + Constants.EDITOR_CONTENTS_CACHE_SIZE, 0); + if (null == textBeforeCursor) { + mExpectedSelStart = mExpectedSelEnd = Constants.NOT_A_CURSOR_POSITION; + } else { + final int textLength = textBeforeCursor.length(); + if (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE + && (textLength > mExpectedSelStart + || mExpectedSelStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) { + // It should not be possible to have only one of those variables be + // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized + // (simple cursor, no selection) or there is no cursor/we don't know its pos + final boolean wasEqual = mExpectedSelStart == mExpectedSelEnd; + mExpectedSelStart = textLength; + // We can't figure out the value of mLastSelectionEnd :( + // But at least if it's smaller than mLastSelectionStart something is wrong, + // and if they used to be equal we also don't want to make it look like there is a + // selection. + if (wasEqual || mExpectedSelStart > mExpectedSelEnd) { + mExpectedSelEnd = mExpectedSelStart; + } + } + } + } + + public int getExpectedSelectionStart() { + return mExpectedSelStart; + } + + public int getExpectedSelectionEnd() { + return mExpectedSelEnd; + } + + /** + * @return whether there is a selection currently active. + */ + public boolean hasSelection() { + return mExpectedSelEnd != mExpectedSelStart; + } } diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java index 6b6bbf3a7..630a03670 100644 --- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java +++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java @@ -50,7 +50,7 @@ public final class RichInputMethodManager { private static final RichInputMethodManager sInstance = new RichInputMethodManager(); private InputMethodManagerCompatWrapper mImmWrapper; - private InputMethodInfo mInputMethodInfoOfThisIme; + private InputMethodInfoCache mInputMethodInfoCache; final HashMap<InputMethodInfo, List<InputMethodSubtype>> mSubtypeListCacheWithImplicitlySelectedSubtypes = CollectionUtils.newHashMap(); final HashMap<InputMethodInfo, List<InputMethodSubtype>> @@ -83,7 +83,8 @@ public final class RichInputMethodManager { return; } mImmWrapper = new InputMethodManagerCompatWrapper(context); - mInputMethodInfoOfThisIme = getInputMethodInfoOfThisIme(context); + mInputMethodInfoCache = new InputMethodInfoCache( + mImmWrapper.mImm, context.getPackageName()); // Initialize additional subtypes. SubtypeLocaleUtils.init(context); @@ -99,20 +100,10 @@ public final class RichInputMethodManager { return mImmWrapper.mImm; } - private InputMethodInfo getInputMethodInfoOfThisIme(final Context context) { - final String packageName = context.getPackageName(); - for (final InputMethodInfo imi : mImmWrapper.mImm.getInputMethodList()) { - if (imi.getPackageName().equals(packageName)) { - return imi; - } - } - throw new RuntimeException("Input method id for " + packageName + " not found."); - } - public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList( boolean allowsImplicitlySelectedSubtypes) { - return getEnabledInputMethodSubtypeList(mInputMethodInfoOfThisIme, - allowsImplicitlySelectedSubtypes); + return getEnabledInputMethodSubtypeList( + getInputMethodInfoOfThisIme(), allowsImplicitlySelectedSubtypes); } public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) { @@ -153,10 +144,10 @@ public final class RichInputMethodManager { private boolean switchToNextInputMethodAndSubtype(final IBinder token) { final InputMethodManager imm = mImmWrapper.mImm; final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList(); - final int currentIndex = getImiIndexInList(mInputMethodInfoOfThisIme, enabledImis); + final int currentIndex = getImiIndexInList(getInputMethodInfoOfThisIme(), enabledImis); if (currentIndex == INDEX_NOT_FOUND) { Log.w(TAG, "Can't find current IME in enabled IMEs: IME package=" - + mInputMethodInfoOfThisIme.getPackageName()); + + getInputMethodInfoOfThisIme().getPackageName()); return false; } final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis); @@ -213,16 +204,45 @@ public final class RichInputMethodManager { return true; } + private static class InputMethodInfoCache { + private final InputMethodManager mImm; + private final String mImePackageName; + + private InputMethodInfo mCachedValue; + + public InputMethodInfoCache(final InputMethodManager imm, final String imePackageName) { + mImm = imm; + mImePackageName = imePackageName; + } + + public synchronized InputMethodInfo get() { + if (mCachedValue != null) { + return mCachedValue; + } + for (final InputMethodInfo imi : mImm.getInputMethodList()) { + if (imi.getPackageName().equals(mImePackageName)) { + mCachedValue = imi; + return imi; + } + } + throw new RuntimeException("Input method id for " + mImePackageName + " not found."); + } + + public synchronized void clear() { + mCachedValue = null; + } + } + public InputMethodInfo getInputMethodInfoOfThisIme() { - return mInputMethodInfoOfThisIme; + return mInputMethodInfoCache.get(); } public String getInputMethodIdOfThisIme() { - return mInputMethodInfoOfThisIme.getId(); + return getInputMethodInfoOfThisIme().getId(); } public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) { - return checkIfSubtypeBelongsToImeAndEnabled(mInputMethodInfoOfThisIme, subtype); + return checkIfSubtypeBelongsToImeAndEnabled(getInputMethodInfoOfThisIme(), subtype); } public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled( @@ -258,7 +278,7 @@ public final class RichInputMethodManager { } public boolean checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype) { - return getSubtypeIndexInIme(subtype, mInputMethodInfoOfThisIme) != INDEX_NOT_FOUND; + return getSubtypeIndexInIme(subtype, getInputMethodInfoOfThisIme()) != INDEX_NOT_FOUND; } private static int getSubtypeIndexInIme(final InputMethodSubtype subtype, @@ -286,7 +306,8 @@ public final class RichInputMethodManager { public boolean hasMultipleEnabledSubtypesInThisIme( final boolean shouldIncludeAuxiliarySubtypes) { - final List<InputMethodInfo> imiList = Collections.singletonList(mInputMethodInfoOfThisIme); + final List<InputMethodInfo> imiList = Collections.singletonList( + getInputMethodInfoOfThisIme()); return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList); } @@ -340,7 +361,7 @@ public final class RichInputMethodManager { public InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(final String localeString, final String keyboardLayoutSetName) { - final InputMethodInfo myImi = mInputMethodInfoOfThisIme; + final InputMethodInfo myImi = getInputMethodInfoOfThisIme(); final int count = myImi.getSubtypeCount(); for (int i = 0; i < count; i++) { final InputMethodSubtype subtype = myImi.getSubtypeAt(i); @@ -355,13 +376,14 @@ public final class RichInputMethodManager { public void setInputMethodAndSubtype(final IBinder token, final InputMethodSubtype subtype) { mImmWrapper.mImm.setInputMethodAndSubtype( - token, mInputMethodInfoOfThisIme.getId(), subtype); + token, getInputMethodIdOfThisIme(), subtype); } public void setAdditionalInputMethodSubtypes(final InputMethodSubtype[] subtypes) { mImmWrapper.mImm.setAdditionalInputMethodSubtypes( - mInputMethodInfoOfThisIme.getId(), subtypes); - // Clear the cache so that we go read the subtypes again next time. + getInputMethodIdOfThisIme(), subtypes); + // Clear the cache so that we go read the {@link InputMethodInfo} of this IME and list of + // subtypes again next time. clearSubtypeCaches(); } @@ -382,5 +404,6 @@ public final class RichInputMethodManager { public void clearSubtypeCaches() { mSubtypeListCacheWithImplicitlySelectedSubtypes.clear(); mSubtypeListCacheWithoutImplicitlySelectedSubtypes.clear(); + mInputMethodInfoCache.clear(); } } diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java index cd9c89f04..021133945 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -32,12 +32,17 @@ import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.keyboard.internal.LanguageOnSpacebarHelper; +import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; public final class SubtypeSwitcher { private static boolean DBG = LatinImeLogger.sDBG; @@ -49,47 +54,42 @@ public final class SubtypeSwitcher { private /* final */ Resources mResources; private /* final */ ConnectivityManager mConnectivityManager; - private final NeedsToDisplayLanguage mNeedsToDisplayLanguage = new NeedsToDisplayLanguage(); + private final LanguageOnSpacebarHelper mLanguageOnSpacebarHelper = + new LanguageOnSpacebarHelper(); private InputMethodInfo mShortcutInputMethodInfo; private InputMethodSubtype mShortcutSubtype; private InputMethodSubtype mNoLanguageSubtype; private InputMethodSubtype mEmojiSubtype; private boolean mIsNetworkConnected; + private static final String KEYBOARD_MODE = "keyboard"; // Dummy no language QWERTY subtype. See {@link R.xml.method}. - private static final InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE = new InputMethodSubtype( - R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark, - SubtypeLocaleUtils.NO_LANGUAGE, "keyboard", "KeyboardLayoutSet=" - + SubtypeLocaleUtils.QWERTY - + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE - + ",EnabledWhenDefaultIsNotAsciiCapable," - + Constants.Subtype.ExtraValue.EMOJI_CAPABLE, - false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */); + private static final int SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE = 0xdde0bfd3; + private static final String EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE = + "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY + + "," + 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( + 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); // 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 InputMethodSubtype DUMMY_EMOJI_SUBTYPE = new InputMethodSubtype( - R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark, - SubtypeLocaleUtils.NO_LANGUAGE, "keyboard", "KeyboardLayoutSet=" - + SubtypeLocaleUtils.EMOJI + "," - + Constants.Subtype.ExtraValue.EMOJI_CAPABLE, - false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */); - - static final class NeedsToDisplayLanguage { - private int mEnabledSubtypeCount; - private boolean mIsSystemLanguageSameAsInputLanguage; - - public boolean getValue() { - return mEnabledSubtypeCount >= 2 || !mIsSystemLanguageSameAsInputLanguage; - } - - public void updateEnabledSubtypeCount(final int count) { - mEnabledSubtypeCount = count; - } - - public void updateIsSystemLanguageSameAsInputLanguage(final boolean isSame) { - mIsSystemLanguageSameAsInputLanguage = isSame; - } - } + 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 = + 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); public static SubtypeSwitcher getInstance() { return sInstance; @@ -128,7 +128,7 @@ public final class SubtypeSwitcher { public void updateParametersOnStartInputView() { final List<InputMethodSubtype> enabledSubtypesOfThisIme = mRichImm.getMyEnabledInputMethodSubtypeList(true); - mNeedsToDisplayLanguage.updateEnabledSubtypeCount(enabledSubtypesOfThisIme.size()); + mLanguageOnSpacebarHelper.updateEnabledSubtypes(enabledSubtypesOfThisIme); updateShortcutIME(); } @@ -177,7 +177,7 @@ public final class SubtypeSwitcher { final boolean sameLanguage = systemLocale.getLanguage().equals(newLocale.getLanguage()); final boolean implicitlyEnabled = mRichImm.checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype); - mNeedsToDisplayLanguage.updateIsSystemLanguageSameAsInputLanguage( + mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage( sameLocale || (sameLanguage && implicitlyEnabled)); updateShortcutIME(); @@ -213,6 +213,7 @@ public final class SubtypeSwitcher { } public boolean isShortcutImeEnabled() { + updateShortcutIME(); if (mShortcutInputMethodInfo == null) { return false; } @@ -224,10 +225,13 @@ public final class SubtypeSwitcher { } public boolean isShortcutImeReady() { - if (mShortcutInputMethodInfo == null) + updateShortcutIME(); + if (mShortcutInputMethodInfo == null) { return false; - if (mShortcutSubtype == null) + } + if (mShortcutSubtype == null) { return true; + } if (mShortcutSubtype.containsExtraValueKey(REQ_NETWORK_CONNECTIVITY)) { return mIsNetworkConnected; } @@ -246,28 +250,53 @@ public final class SubtypeSwitcher { // Subtype Switching functions // ////////////////////////////////// - public boolean needsToDisplayLanguage(final Locale keyboardLocale) { - if (keyboardLocale.toString().equals(SubtypeLocaleUtils.NO_LANGUAGE)) { - return true; + public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) { + return mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(subtype); + } + + public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes() { + final Locale systemLocale = mResources.getConfiguration().locale; + final Set<InputMethodSubtype> enabledSubtypesOfEnabledImes = + new HashSet<InputMethodSubtype>(); + final InputMethodManager inputMethodManager = mRichImm.getInputMethodManager(); + final List<InputMethodInfo> enabledInputMethodInfoList = + inputMethodManager.getEnabledInputMethodList(); + for (final InputMethodInfo info : enabledInputMethodInfoList) { + final List<InputMethodSubtype> enabledSubtypes = + inputMethodManager.getEnabledInputMethodSubtypeList( + info, true /* allowsImplicitlySelectedSubtypes */); + if (enabledSubtypes.isEmpty()) { + // An IME with no subtypes is found. + return false; + } + enabledSubtypesOfEnabledImes.addAll(enabledSubtypes); } - if (!keyboardLocale.equals(getCurrentSubtypeLocale())) { - return false; + for (final InputMethodSubtype subtype : enabledSubtypesOfEnabledImes) { + if (!subtype.isAuxiliary() && !subtype.getLocale().isEmpty() + && !systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) { + return false; + } } - return mNeedsToDisplayLanguage.getValue(); + return true; } - private static Locale sForcedLocaleForTesting = null; + private static InputMethodSubtype sForcedSubtypeForTesting = null; @UsedForTesting - void forceLocale(final Locale locale) { - sForcedLocaleForTesting = locale; + void forceSubtype(final InputMethodSubtype subtype) { + sForcedSubtypeForTesting = subtype; } public Locale getCurrentSubtypeLocale() { - if (null != sForcedLocaleForTesting) return sForcedLocaleForTesting; + if (null != sForcedSubtypeForTesting) { + return LocaleUtils.constructLocaleFromString(sForcedSubtypeForTesting.getLocale()); + } return SubtypeLocaleUtils.getSubtypeLocale(getCurrentSubtype()); } public InputMethodSubtype getCurrentSubtype() { + if (null != sForcedSubtypeForTesting) { + return sForcedSubtypeForTesting; + } return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype()); } @@ -279,8 +308,8 @@ public final class SubtypeSwitcher { if (mNoLanguageSubtype != null) { return mNoLanguageSubtype; } - Log.w(TAG, "Can't find no lanugage with QWERTY subtype"); - Log.w(TAG, "No input method subtype found; return dummy subtype: " + Log.w(TAG, "Can't find any language with QWERTY subtype"); + Log.w(TAG, "No input method subtype found; returning dummy subtype: " + DUMMY_NO_LANGUAGE_SUBTYPE); return DUMMY_NO_LANGUAGE_SUBTYPE; } @@ -293,8 +322,9 @@ public final class SubtypeSwitcher { if (mEmojiSubtype != null) { return mEmojiSubtype; } - Log.w(TAG, "Can't find Emoji subtype"); - Log.w(TAG, "No input method subtype found; return dummy subtype: " + DUMMY_EMOJI_SUBTYPE); + Log.w(TAG, "Can't find emoji subtype"); + Log.w(TAG, "No input method subtype found; returning dummy subtype: " + + DUMMY_EMOJI_SUBTYPE); return DUMMY_EMOJI_SUBTYPE; } } diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java index 0a4c7a55d..db0a8a81c 100644 --- a/java/src/com/android/inputmethod/latin/Suggest.java +++ b/java/src/com/android/inputmethod/latin/Suggest.java @@ -16,28 +16,20 @@ package com.android.inputmethod.latin; -import android.content.Context; -import android.preference.PreferenceManager; import android.text.TextUtils; -import android.util.Log; -import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.event.Event; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.personalization.PersonalizationDictionary; -import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary; -import com.android.inputmethod.latin.personalization.UserHistoryDictionary; -import com.android.inputmethod.latin.settings.Settings; +import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.latin.utils.AutoCorrectionUtils; -import com.android.inputmethod.latin.utils.BoundedTreeSet; +import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; import com.android.inputmethod.latin.utils.CollectionUtils; import com.android.inputmethod.latin.utils.StringUtils; +import com.android.inputmethod.latin.utils.SuggestionResults; import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; import java.util.Locale; -import java.util.concurrent.ConcurrentHashMap; /** * This class loads a dictionary and provides a list of suggestions for a given sequence of @@ -60,153 +52,17 @@ public final class Suggest { // Close to -2**31 private static final int SUPPRESS_SUGGEST_THRESHOLD = -2000000000; - public static final int MAX_SUGGESTIONS = 18; - - public interface SuggestInitializationListener { - public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); - } - private static final boolean DBG = LatinImeLogger.sDBG; - - private final ConcurrentHashMap<String, Dictionary> mDictionaries = - CollectionUtils.newConcurrentHashMap(); - private HashSet<String> mOnlyDictionarySetForDebug = null; - private Dictionary mMainDictionary; - private ContactsBinaryDictionary mContactsDict; - @UsedForTesting - private boolean mIsCurrentlyWaitingForMainDictionary = false; + public final DictionaryFacilitatorForSuggest mDictionaryFacilitator = + new DictionaryFacilitatorForSuggest(); private float mAutoCorrectionThreshold; - // Locale used for upper- and title-casing words - public final Locale mLocale; - - public Suggest(final Context context, final Locale locale, - final SuggestInitializationListener listener) { - initAsynchronously(context, locale, listener); - mLocale = locale; - // initialize a debug flag for the personalization - if (Settings.readUseOnlyPersonalizationDictionaryForDebug( - PreferenceManager.getDefaultSharedPreferences(context))) { - mOnlyDictionarySetForDebug = new HashSet<String>(); - mOnlyDictionarySetForDebug.add(Dictionary.TYPE_PERSONALIZATION); - mOnlyDictionarySetForDebug.add(Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA); - } - } - - @UsedForTesting - Suggest(final AssetFileAddress[] dictionaryList, final Locale locale) { - final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(dictionaryList, - false /* useFullEditDistance */, locale); - mLocale = locale; - mMainDictionary = mainDict; - addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, mainDict); - } - - private void initAsynchronously(final Context context, final Locale locale, - final SuggestInitializationListener listener) { - resetMainDict(context, locale, listener); - } - - private void addOrReplaceDictionaryInternal(final String key, final Dictionary dict) { - if (mOnlyDictionarySetForDebug != null && !mOnlyDictionarySetForDebug.contains(key)) { - Log.w(TAG, "Ignore add " + key + " dictionary for debug."); - return; - } - addOrReplaceDictionary(mDictionaries, key, dict); - } - - private static void addOrReplaceDictionary( - final ConcurrentHashMap<String, Dictionary> dictionaries, - final String key, final Dictionary dict) { - final Dictionary oldDict = (dict == null) - ? dictionaries.remove(key) - : dictionaries.put(key, dict); - if (oldDict != null && dict != oldDict) { - oldDict.close(); - } - } - - public void resetMainDict(final Context context, final Locale locale, - final SuggestInitializationListener listener) { - mIsCurrentlyWaitingForMainDictionary = true; - mMainDictionary = null; - if (listener != null) { - listener.onUpdateMainDictionaryAvailability(hasMainDictionary()); - } - new Thread("InitializeBinaryDictionary") { - @Override - public void run() { - final DictionaryCollection newMainDict = - DictionaryFactory.createMainDictionaryFromManager(context, locale); - addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, newMainDict); - mMainDictionary = newMainDict; - if (listener != null) { - listener.onUpdateMainDictionaryAvailability(hasMainDictionary()); - } - mIsCurrentlyWaitingForMainDictionary = false; - } - }.start(); - } - - // The main dictionary could have been loaded asynchronously. Don't cache the return value - // of this method. - public boolean hasMainDictionary() { - return null != mMainDictionary && mMainDictionary.isInitialized(); - } - - @UsedForTesting - public boolean isCurrentlyWaitingForMainDictionary() { - return mIsCurrentlyWaitingForMainDictionary; + public Locale getLocale() { + return mDictionaryFacilitator.getLocale(); } - public Dictionary getMainDictionary() { - return mMainDictionary; - } - - public ContactsBinaryDictionary getContactsDictionary() { - return mContactsDict; - } - - public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() { - return mDictionaries; - } - - /** - * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted - * before the main dictionary, if set. This refers to the system-managed user dictionary. - */ - public void setUserDictionary(final UserBinaryDictionary userDictionary) { - addOrReplaceDictionaryInternal(Dictionary.TYPE_USER, userDictionary); - } - - /** - * Sets an optional contacts dictionary resource to be loaded. It is also possible to remove - * the contacts dictionary by passing null to this method. In this case no contacts dictionary - * won't be used. - */ - public void setContactsDictionary(final ContactsBinaryDictionary contactsDictionary) { - mContactsDict = contactsDictionary; - addOrReplaceDictionaryInternal(Dictionary.TYPE_CONTACTS, contactsDictionary); - } - - public void setUserHistoryDictionary(final UserHistoryDictionary userHistoryDictionary) { - addOrReplaceDictionaryInternal(Dictionary.TYPE_USER_HISTORY, userHistoryDictionary); - } - - public void setPersonalizationPredictionDictionary( - final PersonalizationPredictionDictionary personalizationPredictionDictionary) { - addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA, - personalizationPredictionDictionary); - } - - public void setPersonalizationDictionary( - final PersonalizationDictionary personalizationDictionary) { - addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION, - personalizationDictionary); - } - - public void setAutoCorrectionThreshold(float threshold) { + public void setAutoCorrectionThreshold(final float threshold) { mAutoCorrectionThreshold = threshold; } @@ -239,47 +95,53 @@ public final class Suggest { final int[] additionalFeaturesOptions, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { final int trailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount(); - final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator, - MAX_SUGGESTIONS); - final String typedWord = wordComposer.getTypedWord(); final String consideredWord = trailingSingleQuotesCount > 0 ? typedWord.substring(0, typedWord.length() - trailingSingleQuotesCount) : typedWord; LatinImeLogger.onAddSuggestedWord(typedWord, Dictionary.TYPE_USER_TYPED); - final WordComposer wordComposerForLookup; - if (trailingSingleQuotesCount > 0) { - wordComposerForLookup = new WordComposer(wordComposer); - for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) { - wordComposerForLookup.deleteLast(); - } + final ArrayList<SuggestedWordInfo> rawSuggestions; + if (ProductionFlag.INCLUDE_RAW_SUGGESTIONS) { + rawSuggestions = CollectionUtils.newArrayList(); } else { - wordComposerForLookup = wordComposer; - } - - for (final String key : mDictionaries.keySet()) { - final Dictionary dictionary = mDictionaries.get(key); - suggestionsSet.addAll(dictionary.getSuggestions(wordComposerForLookup, - prevWordForBigram, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions)); + rawSuggestions = null; } + final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( + wordComposer, prevWordForBigram, proximityInfo, blockOffensiveWords, + additionalFeaturesOptions, SESSION_TYPING, rawSuggestions); + final boolean isFirstCharCapitalized = wordComposer.isFirstCharCapitalized(); + final boolean isAllUpperCase = wordComposer.isAllUpperCase(); + final String firstSuggestion; final String whitelistedWord; - if (suggestionsSet.isEmpty()) { - whitelistedWord = null; - } else if (SuggestedWordInfo.KIND_WHITELIST != suggestionsSet.first().mKind) { - whitelistedWord = null; + if (suggestionResults.isEmpty()) { + whitelistedWord = firstSuggestion = null; } else { - whitelistedWord = suggestionsSet.first().mWord; + final SuggestedWordInfo firstSuggestedWordInfo = getTransformedSuggestedWordInfo( + suggestionResults.first(), suggestionResults.mLocale, isAllUpperCase, + isFirstCharCapitalized, trailingSingleQuotesCount); + firstSuggestion = firstSuggestedWordInfo.mWord; + if (SuggestedWordInfo.KIND_WHITELIST != firstSuggestedWordInfo.mKind) { + whitelistedWord = null; + } else { + whitelistedWord = firstSuggestion; + } } - // The word can be auto-corrected if it has a whitelist entry that is not itself, - // or if it's a 2+ characters non-word (i.e. it's not in the dictionary). + final boolean isPrediction = !wordComposer.isComposingWord(); + + // We allow auto-correction if we have a whitelisted word, or if the word is not a valid + // word of more than 1 char, except if the first suggestion is the same as the typed string + // because in this case if it's strong enough to auto-correct that will mistakenly designate + // the second candidate for auto-correction. + // TODO: stop relying on indices to find where is the auto-correction in the suggested + // words, and correct this test. final boolean allowsToBeAutoCorrected = (null != whitelistedWord - && !whitelistedWord.equals(consideredWord)) - || (consideredWord.length() > 1 && !AutoCorrectionUtils.isValidWord(this, - consideredWord, wordComposer.isFirstCharCapitalized())); + && !whitelistedWord.equals(typedWord)) + || (consideredWord.length() > 1 && !mDictionaryFacilitator.isValidWord( + consideredWord, wordComposer.isFirstCharCapitalized()) + && !typedWord.equals(firstSuggestion)); final boolean hasAutoCorrection; // TODO: using isCorrectionEnabled here is not very good. It's probably useless, because @@ -287,10 +149,11 @@ public final class Suggest { // same time, it feels wrong that the SuggestedWord object includes information about // the current settings. It may also be useful to know, when the setting is off, whether // the word *would* have been auto-corrected. - if (!isCorrectionEnabled || !allowsToBeAutoCorrected || !wordComposer.isComposingWord() - || suggestionsSet.isEmpty() || wordComposer.hasDigits() - || wordComposer.isMostlyCaps() || wordComposer.isResumed() || !hasMainDictionary() - || SuggestedWordInfo.KIND_SHORTCUT == suggestionsSet.first().mKind) { + if (!isCorrectionEnabled || !allowsToBeAutoCorrected || isPrediction + || suggestionResults.isEmpty() || wordComposer.hasDigits() + || wordComposer.isMostlyCaps() || wordComposer.isResumed() + || !mDictionaryFacilitator.hasInitializedMainDictionary() + || SuggestedWordInfo.KIND_SHORTCUT == suggestionResults.first().mKind) { // 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 // their language, and it will unexpectedly auto-correct. For example, if the user @@ -302,19 +165,17 @@ public final class Suggest { hasAutoCorrection = false; } else { hasAutoCorrection = AutoCorrectionUtils.suggestionExceedsAutoCorrectionThreshold( - suggestionsSet.first(), consideredWord, mAutoCorrectionThreshold); + suggestionResults.first(), consideredWord, mAutoCorrectionThreshold); } final ArrayList<SuggestedWordInfo> suggestionsContainer = - CollectionUtils.newArrayList(suggestionsSet); + CollectionUtils.newArrayList(suggestionResults); final int suggestionsCount = suggestionsContainer.size(); - final boolean isFirstCharCapitalized = wordComposer.isFirstCharCapitalized(); - final boolean isAllUpperCase = wordComposer.isAllUpperCase(); if (isFirstCharCapitalized || isAllUpperCase || 0 != trailingSingleQuotesCount) { for (int i = 0; i < suggestionsCount; ++i) { final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo( - wordInfo, mLocale, isAllUpperCase, isFirstCharCapitalized, + wordInfo, suggestionResults.mLocale, isAllUpperCase, isFirstCharCapitalized, trailingSingleQuotesCount); suggestionsContainer.set(i, transformedWordInfo); } @@ -342,15 +203,13 @@ public final class Suggest { suggestionsList = suggestionsContainer; } - callback.onGetSuggestedWords(new SuggestedWords(suggestionsList, + callback.onGetSuggestedWords(new SuggestedWords(suggestionsList, rawSuggestions, // TODO: this first argument is lying. If this is a whitelisted word which is an // actual word, it says typedWordValid = false, which looks wrong. We should either // rename the attribute or change the value. - !allowsToBeAutoCorrected /* typedWordValid */, + !isPrediction && !allowsToBeAutoCorrected /* typedWordValid */, hasAutoCorrection, /* willAutoCorrect */ - false /* isPunctuationSuggestions */, - false /* isObsoleteSuggestions */, - !wordComposer.isComposingWord() /* isPrediction */, sequenceNumber)); + false /* isObsoleteSuggestions */, isPrediction, sequenceNumber)); } // Retrieves suggestions for the batch input @@ -360,23 +219,21 @@ public final class Suggest { final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, final int sessionId, final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { - final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator, - MAX_SUGGESTIONS); - - // At second character typed, search the unigrams (scores being affected by bigrams) - for (final String key : mDictionaries.keySet()) { - final Dictionary dictionary = mDictionaries.get(key); - suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId(wordComposer, - prevWordForBigram, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions, sessionId)); + final ArrayList<SuggestedWordInfo> rawSuggestions; + if (ProductionFlag.INCLUDE_RAW_SUGGESTIONS) { + rawSuggestions = CollectionUtils.newArrayList(); + } else { + rawSuggestions = null; } - - for (SuggestedWordInfo wordInfo : suggestionsSet) { + final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( + wordComposer, prevWordForBigram, proximityInfo, blockOffensiveWords, + additionalFeaturesOptions, sessionId, rawSuggestions); + for (SuggestedWordInfo wordInfo : suggestionResults) { LatinImeLogger.onAddSuggestedWord(wordInfo.mWord, wordInfo.mSourceDict.mDictType); } final ArrayList<SuggestedWordInfo> suggestionsContainer = - CollectionUtils.newArrayList(suggestionsSet); + CollectionUtils.newArrayList(suggestionResults); final int suggestionsCount = suggestionsContainer.size(); final boolean isFirstCharCapitalized = wordComposer.wasShiftedNoLock(); final boolean isAllUpperCase = wordComposer.isAllUpperCase(); @@ -384,7 +241,7 @@ public final class Suggest { for (int i = 0; i < suggestionsCount; ++i) { final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo( - wordInfo, mLocale, isAllUpperCase, isFirstCharCapitalized, + wordInfo, suggestionResults.mLocale, isAllUpperCase, isFirstCharCapitalized, 0 /* trailingSingleQuotesCount */); suggestionsContainer.set(i, transformedWordInfo); } @@ -407,10 +264,9 @@ public final class Suggest { // In the batch input mode, the most relevant suggested word should act as a "typed word" // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false). - callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer, + callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer, rawSuggestions, true /* typedWordValid */, false /* willAutoCorrect */, - false /* isPunctuationSuggestions */, false /* isObsoleteSuggestions */, false /* isPrediction */, sequenceNumber)); } @@ -427,12 +283,13 @@ public final class Suggest { // than i because we added the typed word to mSuggestions without touching mScores. for (int i = 0; i < suggestionsSize - 1; ++i) { final SuggestedWordInfo cur = suggestions.get(i + 1); - final float normalizedScore = BinaryDictionary.calcNormalizedScore( + final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( typedWord, cur.toString(), cur.mScore); final String scoreInfoString; if (normalizedScore > 0) { scoreInfoString = String.format( - Locale.ROOT, "%d (%4.2f)", cur.mScore, normalizedScore); + Locale.ROOT, "%d (%4.2f), %s", cur.mScore, normalizedScore, + cur.mSourceDict.mDictType); } else { scoreInfoString = Integer.toString(cur.mScore); } @@ -442,22 +299,6 @@ public final class Suggest { return suggestionsList; } - private static final class SuggestedWordInfoComparator - implements Comparator<SuggestedWordInfo> { - // This comparator ranks the word info with the higher frequency first. That's because - // that's the order we want our elements in. - @Override - public int compare(final SuggestedWordInfo o1, final SuggestedWordInfo o2) { - if (o1.mScore > o2.mScore) return -1; - if (o1.mScore < o2.mScore) return 1; - if (o1.mCodePointCount < o2.mCodePointCount) return -1; - if (o1.mCodePointCount > o2.mCodePointCount) return 1; - return o1.mWord.compareTo(o2.mWord); - } - } - private static final SuggestedWordInfoComparator sSuggestedWordInfoComparator = - new SuggestedWordInfoComparator(); - /* package for test */ static SuggestedWordInfo getTransformedSuggestedWordInfo( final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase, final boolean isFirstCharCapitalized, final int trailingSingleQuotesCount) { @@ -481,13 +322,4 @@ public final class Suggest { wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord, wordInfo.mAutoCommitFirstWordConfidence); } - - public void close() { - final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet(); - dictionaries.addAll(mDictionaries.values()); - for (final Dictionary dictionary : dictionaries) { - dictionary.close(); - } - mMainDictionary = null; - } } diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index 97c89dd4e..dc2c9fd0e 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -26,51 +26,71 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; -public final class SuggestedWords { +public class SuggestedWords { public static final int INDEX_OF_TYPED_WORD = 0; public static final int INDEX_OF_AUTO_CORRECTION = 1; public static final int NOT_A_SEQUENCE_NUMBER = -1; + // The maximum number of suggestions available. + public static final int MAX_SUGGESTIONS = 18; + private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = CollectionUtils.newArrayList(0); public static final SuggestedWords EMPTY = new SuggestedWords( - EMPTY_WORD_INFO_LIST, false, false, false, false, false); + EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false, false, false, false); + public final String mTypedWord; public final boolean mTypedWordValid; // Note: this INCLUDES cases where the word will auto-correct to itself. A good definition // of what this flag means would be "the top suggestion is strong enough to auto-correct", // whether this exactly matches the user entry or not. public final boolean mWillAutoCorrect; - public final boolean mIsPunctuationSuggestions; public final boolean mIsObsoleteSuggestions; public final boolean mIsPrediction; public final int mSequenceNumber; // Sequence number for auto-commit. - private final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList; + protected final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList; + public final ArrayList<SuggestedWordInfo> mRawSuggestions; public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, + final ArrayList<SuggestedWordInfo> rawSuggestions, final boolean typedWordValid, final boolean willAutoCorrect, - final boolean isPunctuationSuggestions, final boolean isObsoleteSuggestions, final boolean isPrediction) { - this(suggestedWordInfoList, typedWordValid, willAutoCorrect, isPunctuationSuggestions, + this(suggestedWordInfoList, rawSuggestions, typedWordValid, willAutoCorrect, isObsoleteSuggestions, isPrediction, NOT_A_SEQUENCE_NUMBER); } public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, + final ArrayList<SuggestedWordInfo> rawSuggestions, + final boolean typedWordValid, + final boolean willAutoCorrect, + final boolean isObsoleteSuggestions, + final boolean isPrediction, + final int sequenceNumber) { + this(suggestedWordInfoList, rawSuggestions, + (suggestedWordInfoList.isEmpty() || isPrediction) ? null + : suggestedWordInfoList.get(INDEX_OF_TYPED_WORD).mWord, + typedWordValid, willAutoCorrect, isObsoleteSuggestions, isPrediction, + sequenceNumber); + } + + public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, + final ArrayList<SuggestedWordInfo> rawSuggestions, + final String typedWord, final boolean typedWordValid, final boolean willAutoCorrect, - final boolean isPunctuationSuggestions, final boolean isObsoleteSuggestions, final boolean isPrediction, final int sequenceNumber) { mSuggestedWordInfoList = suggestedWordInfoList; + mRawSuggestions = rawSuggestions; mTypedWordValid = typedWordValid; mWillAutoCorrect = willAutoCorrect; - mIsPunctuationSuggestions = isPunctuationSuggestions; mIsObsoleteSuggestions = isObsoleteSuggestions; mIsPrediction = isPrediction; mSequenceNumber = sequenceNumber; + mTypedWord = typedWord; } public boolean isEmpty() { @@ -81,10 +101,32 @@ public final class SuggestedWords { return mSuggestedWordInfoList.size(); } + /** + * Get suggested word at <code>index</code>. + * @param index The index of the suggested word. + * @return The suggested word. + */ public String getWord(final int index) { return mSuggestedWordInfoList.get(index).mWord; } + /** + * Get displayed text at <code>index</code>. + * In RTL languages, the displayed text on the suggestion strip may be different from the + * suggested word that is returned from {@link #getWord(int)}. For example the displayed text + * of punctuation suggestion "(" should be ")". + * @param index The index of the text to display. + * @return The text to be displayed. + */ + public String getLabel(final int index) { + return mSuggestedWordInfoList.get(index).mWord; + } + + /** + * Get {@link SuggestedWordInfo} object at <code>index</code>. + * @param index The index of the {@link SuggestedWordInfo}. + * @return The {@link SuggestedWordInfo} object. + */ public SuggestedWordInfo getInfo(final int index) { return mSuggestedWordInfoList.get(index); } @@ -104,8 +146,12 @@ public final class SuggestedWords { return debugString; } - public boolean willAutoCorrect() { - return mWillAutoCorrect; + /** + * The predicator to tell whether this object represents punctuation suggestions. + * @return false if this object desn't represent punctuation suggestions. + */ + public boolean isPunctuationSuggestions() { + return false; } @Override @@ -114,7 +160,6 @@ public final class SuggestedWords { return "SuggestedWords:" + " mTypedWordValid=" + mTypedWordValid + " mWillAutoCorrect=" + mWillAutoCorrect - + " mIsPunctuationSuggestions=" + mIsPunctuationSuggestions + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray()); } @@ -122,15 +167,10 @@ public final class SuggestedWords { final CompletionInfo[] infos) { final ArrayList<SuggestedWordInfo> result = CollectionUtils.newArrayList(); for (final CompletionInfo info : infos) { - if (info == null) continue; - final CharSequence text = info.getText(); - if (null == text) continue; - final SuggestedWordInfo suggestedWordInfo = new SuggestedWordInfo(text.toString(), - SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_APP_DEFINED, - Dictionary.DICTIONARY_APPLICATION_DEFINED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); - result.add(suggestedWordInfo); + if (null == info || null == info.getText()) { + continue; + } + result.add(new SuggestedWordInfo(info)); } return result; } @@ -150,7 +190,7 @@ public final class SuggestedWords { for (int index = 1; index < previousSize; index++) { final SuggestedWordInfo prevWordInfo = previousSuggestions.getInfo(index); final String prevWord = prevWordInfo.mWord; - // Filter out duplicate suggestion. + // Filter out duplicate suggestions. if (!alreadySeen.contains(prevWord)) { suggestionsList.add(prevWordInfo); alreadySeen.add(prevWord); @@ -189,6 +229,9 @@ public final class SuggestedWords { public static final int KIND_FLAG_EXACT_MATCH = 0x40000000; public final String mWord; + // The completion info from the application. Null for suggestions that don't come from + // the application (including keyboard-computed ones, so this is almost always null) + public final CompletionInfo mApplicationSpecifiedCompletionInfo; public final int mScore; public final int mKind; // one of the KIND_* constants above public final int mCodePointCount; @@ -215,6 +258,7 @@ public final class SuggestedWords { final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord, final int autoCommitFirstWordConfidence) { mWord = word; + mApplicationSpecifiedCompletionInfo = null; mScore = score; mKind = kind; mSourceDict = sourceDict; @@ -223,6 +267,22 @@ public final class SuggestedWords { mAutoCommitFirstWordConfidence = autoCommitFirstWordConfidence; } + /** + * Create a new suggested word info from an application-specified completion. + * If the passed argument or its contained text is null, this throws a NPE. + * @param applicationSpecifiedCompletion The application-specified completion info. + */ + public SuggestedWordInfo(final CompletionInfo applicationSpecifiedCompletion) { + mWord = applicationSpecifiedCompletion.getText().toString(); + mApplicationSpecifiedCompletionInfo = applicationSpecifiedCompletion; + mScore = SuggestedWordInfo.MAX_SCORE; + mKind = SuggestedWordInfo.KIND_APP_DEFINED; + mSourceDict = Dictionary.DICTIONARY_APPLICATION_DEFINED; + mCodePointCount = StringUtils.codePointCount(mWord); + mIndexOfTouchPointOfSecondWord = SuggestedWordInfo.NOT_AN_INDEX; + mAutoCommitFirstWordConfidence = SuggestedWordInfo.NOT_A_CONFIDENCE; + } + public boolean isEligibleForAutoCommit() { return (KIND_CORRECTION == mKind && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord); } @@ -278,17 +338,21 @@ public final class SuggestedWords { // words from the member ArrayList as some other parties may expect the object to never change. public SuggestedWords getSuggestedWordsExcludingTypedWord() { final ArrayList<SuggestedWordInfo> newSuggestions = CollectionUtils.newArrayList(); + String typedWord = null; for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { final SuggestedWordInfo info = mSuggestedWordInfoList.get(i); if (SuggestedWordInfo.KIND_TYPED != info.mKind) { 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, true /* typedWordValid */, - false /* willAutoCorrect */, mIsPunctuationSuggestions, mIsObsoleteSuggestions, - mIsPrediction); + return new SuggestedWords(newSuggestions, null /* rawSuggestions */, typedWord, + true /* typedWordValid */, false /* willAutoCorrect */, mIsObsoleteSuggestions, + mIsPrediction, NOT_A_SEQUENCE_NUMBER); } // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the @@ -306,8 +370,7 @@ public final class SuggestedWords { info.mSourceDict, SuggestedWordInfo.NOT_AN_INDEX, SuggestedWordInfo.NOT_A_CONFIDENCE)); } - return new SuggestedWords(newSuggestions, mTypedWordValid, - mWillAutoCorrect, mIsPunctuationSuggestions, mIsObsoleteSuggestions, - mIsPrediction); + return new SuggestedWords(newSuggestions, null /* rawSuggestions */, mTypedWordValid, + mWillAutoCorrect, mIsObsoleteSuggestions, mIsPrediction); } } diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java index 3213c92c7..c24ee4033 100644 --- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java @@ -25,34 +25,27 @@ import java.util.ArrayList; import java.util.Locale; public final class SynchronouslyLoadedContactsBinaryDictionary extends ContactsBinaryDictionary { - private boolean mClosed; + private final Object mLock = new Object(); public SynchronouslyLoadedContactsBinaryDictionary(final Context context, final Locale locale) { super(context, locale); } @Override - public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes, + public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes, final String prevWordForBigrams, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - reloadDictionaryIfRequired(); - return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions); + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final float[] inOutLanguageWeight) { + synchronized (mLock) { + return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, + blockOffensiveWords, additionalFeaturesOptions, inOutLanguageWeight); + } } @Override - public synchronized boolean isValidWord(final String word) { - reloadDictionaryIfRequired(); - return isValidWordInner(word); - } - - // Protect against multiple closing - @Override - public synchronized void close() { - // Actually with the current implementation of ContactsDictionary it's safe to close - // several times, so the following protection is really only for foolproofing - if (mClosed) return; - mClosed = true; - super.close(); + public boolean isValidWord(final String word) { + synchronized (mLock) { + return super.isValidWord(word); + } } } diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java index 6405b5e46..1d29d7ad0 100644 --- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java @@ -22,30 +22,35 @@ import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import java.util.ArrayList; +import java.util.Locale; public final class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionary { + private final Object mLock = new Object(); - public SynchronouslyLoadedUserBinaryDictionary(final Context context, final String locale) { - this(context, locale, false); + public SynchronouslyLoadedUserBinaryDictionary(final Context context, final Locale locale) { + this(context, locale, false /* alsoUseMoreRestrictiveLocales */); } - public SynchronouslyLoadedUserBinaryDictionary(final Context context, final String locale, + public SynchronouslyLoadedUserBinaryDictionary(final Context context, final Locale locale, final boolean alsoUseMoreRestrictiveLocales) { - super(context, locale, alsoUseMoreRestrictiveLocales); + super(context, locale, alsoUseMoreRestrictiveLocales, null /* dictFile */); } @Override - public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes, + public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes, final String prevWordForBigrams, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - reloadDictionaryIfRequired(); - return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords, - additionalFeaturesOptions); + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final float[] inOutLanguageWeight) { + synchronized (mLock) { + return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, + blockOffensiveWords, additionalFeaturesOptions, inOutLanguageWeight); + } } @Override - public synchronized boolean isValidWord(final String word) { - reloadDictionaryIfRequired(); - return isValidWordInner(word); + public boolean isValidWord(final String word) { + synchronized (mLock) { + return super.isValidWord(word); + } } } diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java index 15b3d8d02..8838e27c4 100644 --- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java @@ -18,7 +18,6 @@ package com.android.inputmethod.latin; import android.content.ContentProviderClient; import android.content.ContentResolver; -import android.content.ContentUris; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; @@ -33,6 +32,7 @@ import com.android.inputmethod.compat.UserDictionaryCompatUtils; import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; +import java.io.File; import java.util.Arrays; import java.util.Locale; @@ -74,25 +74,28 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { private ContentObserver mObserver; final private String mLocale; final private boolean mAlsoUseMoreRestrictiveLocales; + final public boolean mEnabled; - public UserBinaryDictionary(final Context context, final String locale) { - this(context, locale, false); + public UserBinaryDictionary(final Context context, final Locale locale) { + this(context, locale, false /* alsoUseMoreRestrictiveLocales */, null /* dictFile */); } - public UserBinaryDictionary(final Context context, final String locale, - final boolean alsoUseMoreRestrictiveLocales) { - super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER, - false /* isUpdatable */); + public UserBinaryDictionary(final Context context, final Locale locale, final File dictFile) { + this(context, locale, false /* alsoUseMoreRestrictiveLocales */, dictFile); + } + + public UserBinaryDictionary(final Context context, final Locale locale, + final boolean alsoUseMoreRestrictiveLocales, final File dictFile) { + super(context, getDictName(NAME, locale, dictFile), locale, Dictionary.TYPE_USER, dictFile); if (null == locale) throw new NullPointerException(); // Catch the error earlier - if (SubtypeLocaleUtils.NO_LANGUAGE.equals(locale)) { + final String localeStr = locale.toString(); + if (SubtypeLocaleUtils.NO_LANGUAGE.equals(localeStr)) { // If we don't have a locale, insert into the "all locales" user dictionary. mLocale = USER_DICTIONARY_ALL_LANGUAGES; } else { - mLocale = locale; + mLocale = localeStr; } mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales; - // Perform a managed query. The Activity will handle closing and re-querying the cursor - // when needed. ContentResolver cres = context.getContentResolver(); mObserver = new ContentObserver(null) { @@ -112,7 +115,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { } }; cres.registerContentObserver(Words.CONTENT_URI, true, mObserver); - + mEnabled = readIsEnabled(); loadDictionary(); } @@ -126,7 +129,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { } @Override - public void loadDictionaryAsync() { + public void loadInitialContentsLocked() { // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"], // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3. // This is correct for locale processing. @@ -178,7 +181,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { try { cursor = mContext.getContentResolver().query( Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null); - addWords(cursor); + addWordsLocked(cursor); } catch (final SQLiteException e) { Log.e(TAG, "SQLiteException in the remote User dictionary process.", e); } finally { @@ -190,7 +193,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { } } - public boolean isEnabled() { + private boolean readIsEnabled() { final ContentResolver cr = mContext.getContentResolver(); final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI); if (client != null) { @@ -232,7 +235,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { } } - private void addWords(final Cursor cursor) { + private void addWordsLocked(final Cursor cursor) { final boolean hasShortcutColumn = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; if (cursor == null) return; if (cursor.moveToFirst()) { @@ -246,12 +249,16 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { final int adjustedFrequency = scaleFrequencyFromDefaultToLatinIme(frequency); // Safeguard against adding really long words. if (word.length() < MAX_WORD_LENGTH) { - super.addWord(word, null, adjustedFrequency, 0 /* shortcutFreq */, - false /* isNotAWord */); - } - if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) { - super.addWord(shortcut, word, adjustedFrequency, USER_DICT_SHORTCUT_FREQUENCY, - true /* isNotAWord */); + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addWordDynamicallyLocked(word, adjustedFrequency, null /* shortcutTarget */, + 0 /* shortcutFreq */, false /* isNotAWord */, + false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP); + if (null != shortcut && shortcut.length() < MAX_WORD_LENGTH) { + runGCIfRequiredLocked(true /* mindsBlockByGC */); + addWordDynamicallyLocked(shortcut, adjustedFrequency, word, + USER_DICT_SHORTCUT_FREQUENCY, true /* isNotAWord */, + false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP); + } } cursor.moveToNext(); } @@ -259,12 +266,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary { } @Override - protected boolean hasContentChanged() { - return true; - } - - @Override - protected boolean needsToReloadBeforeWriting() { + protected boolean haveContentsChanged() { return true; } } diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 039dadc66..d755195f2 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -16,11 +16,14 @@ package com.android.inputmethod.latin; -import com.android.inputmethod.keyboard.Key; -import com.android.inputmethod.keyboard.Keyboard; +import com.android.inputmethod.event.CombinerChain; +import com.android.inputmethod.event.Event; +import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.CoordinateUtils; import com.android.inputmethod.latin.utils.StringUtils; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; /** * A place to store the currently composing word with information such as adjacent key codes as well @@ -37,17 +40,16 @@ public final class WordComposer { public static final int CAPS_MODE_AUTO_SHIFTED = 0x5; public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7; - // An array of code points representing the characters typed so far. - // The array is limited to MAX_WORD_LENGTH code points, but mTypedWord extends past that - // and mCodePointSize can go past that. If mCodePointSize is greater than MAX_WORD_LENGTH, - // this just does not contain the associated code points past MAX_WORD_LENGTH. - private int[] mPrimaryKeyCodes; + private CombinerChain mCombinerChain; + + // The list of events that served to compose this string. + private final ArrayList<Event> mEvents; private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH); - // This is the typed word, as a StringBuilder. This has the same contents as mPrimaryKeyCodes - // but under a StringBuilder representation for ease of use, depending on what is more useful - // at any given time. However this is not limited in size, while mPrimaryKeyCodes is limited - // to MAX_WORD_LENGTH code points. - private final StringBuilder mTypedWord; + // The previous word (before the composing word). Used as context for suggestions. May be null + // after resetting and before starting a new composing word, or when there is no context like + // at the start of text for example. It can also be set to null externally when the user + // enters a separator that does not let bigrams across, like a period or a comma. + private String mPreviousWordForSuggestion; private String mAutoCorrection; private boolean mIsResumed; private boolean mIsBatchMode; @@ -60,10 +62,10 @@ public final class WordComposer { private String mRejectedBatchModeSuggestion; // Cache these values for performance + private CharSequence mTypedWordCache; private int mCapsCount; private int mDigitsCount; private int mCapitalizedMode; - private int mTrailingSingleQuotesCount; // This is the number of code points entered so far. This is not limited to MAX_WORD_LENGTH. // In general, this contains the size of mPrimaryKeyCodes, except when this is greater than // MAX_WORD_LENGTH in which case mPrimaryKeyCodes only contain the first MAX_WORD_LENGTH @@ -77,79 +79,83 @@ public final class WordComposer { private boolean mIsFirstCharCapitalized; public WordComposer() { - mPrimaryKeyCodes = new int[MAX_WORD_LENGTH]; - mTypedWord = new StringBuilder(MAX_WORD_LENGTH); + mCombinerChain = new CombinerChain(); + mEvents = CollectionUtils.newArrayList(); mAutoCorrection = null; - mTrailingSingleQuotesCount = 0; mIsResumed = false; mIsBatchMode = false; mCursorPositionWithinWord = 0; mRejectedBatchModeSuggestion = null; - refreshSize(); - } - - public WordComposer(final WordComposer source) { - mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length); - mTypedWord = new StringBuilder(source.mTypedWord); - mInputPointers.copy(source.mInputPointers); - mCapsCount = source.mCapsCount; - mDigitsCount = source.mDigitsCount; - mIsFirstCharCapitalized = source.mIsFirstCharCapitalized; - mCapitalizedMode = source.mCapitalizedMode; - mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount; - mIsResumed = source.mIsResumed; - mIsBatchMode = source.mIsBatchMode; - mCursorPositionWithinWord = source.mCursorPositionWithinWord; - mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion; - refreshSize(); + mPreviousWordForSuggestion = null; + refreshTypedWordCache(); } /** * Clear out the keys registered so far. */ public void reset() { - mTypedWord.setLength(0); + mCombinerChain.reset(); + mEvents.clear(); mAutoCorrection = null; mCapsCount = 0; mDigitsCount = 0; mIsFirstCharCapitalized = false; - mTrailingSingleQuotesCount = 0; mIsResumed = false; mIsBatchMode = false; mCursorPositionWithinWord = 0; mRejectedBatchModeSuggestion = null; - refreshSize(); + mPreviousWordForSuggestion = null; + refreshTypedWordCache(); } - private final void refreshSize() { - mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length()); + private final void refreshTypedWordCache() { + mTypedWordCache = mCombinerChain.getComposingWordWithCombiningFeedback(); + mCodePointSize = Character.codePointCount(mTypedWordCache, 0, mTypedWordCache.length()); } /** * Number of keystrokes in the composing word. * @return the number of keystrokes */ - public final int size() { + // This may be made public if need be, but right now it's not used anywhere + /* package for tests */ int size() { return mCodePointSize; } - public final boolean isComposingWord() { - return size() > 0; - } + /** + * Copy the code points in the typed word to a destination array of ints. + * + * If the array is too small to hold the code points in the typed word, nothing is copied and + * -1 is returned. + * + * @param destination the array of ints. + * @return the number of copied code points. + */ + public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount( + final int[] destination) { + // lastIndex is exclusive + final int lastIndex = mTypedWordCache.length() - trailingSingleQuotesCount(); + if (lastIndex <= 0) { + // The string is empty or contains only single quotes. + return 0; + } - // TODO: make sure that the index should not exceed MAX_WORD_LENGTH - public int getCodeAt(int index) { - if (index >= MAX_WORD_LENGTH) { + // The following function counts the number of code points in the text range which begins + // at index 0 and extends to the character at lastIndex. + final int codePointSize = Character.codePointCount(mTypedWordCache, 0, lastIndex); + if (codePointSize > destination.length) { return -1; } - return mPrimaryKeyCodes[index]; + return StringUtils.copyCodePointsAndReturnCodePointCount(destination, mTypedWordCache, 0, + lastIndex, true /* downCase */); } - public int getCodeBeforeCursor() { - if (mCursorPositionWithinWord < 1 || mCursorPositionWithinWord > mPrimaryKeyCodes.length) { - return Constants.NOT_A_CODE; - } - return mPrimaryKeyCodes[mCursorPositionWithinWord - 1]; + public boolean isSingleLetter() { + return size() == 1; + } + + public final boolean isComposingWord() { + return size() > 0; } public InputPointers getInputPointers() { @@ -163,38 +169,47 @@ public final class WordComposer { } /** - * Add a new keystroke, with the pressed key's code point with the touch point coordinates. + * Process an input event. + * + * All input events should be supported, including software/hardware events, characters as well + * as deletions, multiple inputs and gestures. + * + * @param event the event to process. */ - public void add(final int primaryCode, final int keyX, final int keyY) { + public void processEvent(final Event event) { + final int primaryCode = event.mCodePoint; + final int keyX = event.mX; + final int keyY = event.mY; final int newIndex = size(); - mTypedWord.appendCodePoint(primaryCode); - refreshSize(); + mCombinerChain.processEvent(mEvents, event); + mEvents.add(event); + refreshTypedWordCache(); mCursorPositionWithinWord = mCodePointSize; - if (newIndex < MAX_WORD_LENGTH) { - mPrimaryKeyCodes[newIndex] = primaryCode >= Constants.CODE_SPACE - ? Character.toLowerCase(primaryCode) : primaryCode; - // In the batch input mode, the {@code mInputPointers} holds batch input points and - // shouldn't be overridden by the "typed key" coordinates - // (See {@link #setBatchInputWord}). - if (!mIsBatchMode) { - // TODO: Set correct pointer id and time - mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0); - } + // We may have deleted the last one. + if (0 == mCodePointSize) { + mIsFirstCharCapitalized = false; } - mIsFirstCharCapitalized = isFirstCharCapitalized( - newIndex, primaryCode, mIsFirstCharCapitalized); - if (Character.isUpperCase(primaryCode)) mCapsCount++; - if (Character.isDigit(primaryCode)) mDigitsCount++; - if (Constants.CODE_SINGLE_QUOTE == primaryCode) { - ++mTrailingSingleQuotesCount; - } else { - mTrailingSingleQuotesCount = 0; + if (Constants.CODE_DELETE != event.mKeyCode) { + if (newIndex < MAX_WORD_LENGTH) { + // In the batch input mode, the {@code mInputPointers} holds batch input points and + // shouldn't be overridden by the "typed key" coordinates + // (See {@link #setBatchInputWord}). + if (!mIsBatchMode) { + // TODO: Set correct pointer id and time + mInputPointers.addPointerAt(newIndex, keyX, keyY, 0, 0); + } + } + mIsFirstCharCapitalized = isFirstCharCapitalized( + newIndex, primaryCode, mIsFirstCharCapitalized); + if (Character.isUpperCase(primaryCode)) mCapsCount++; + if (Character.isDigit(primaryCode)) mDigitsCount++; } mAutoCorrection = null; } public void setCursorPositionWithinWord(final int posWithinWord) { mCursorPositionWithinWord = posWithinWord; + // TODO: compute where that puts us inside the events } public boolean isCursorFrontOrMiddleOfComposingWord() { @@ -215,17 +230,12 @@ public final class WordComposer { * @return true if the cursor is still inside the composing word, false otherwise. */ public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) { + // TODO: should uncommit the composing feedback + mCombinerChain.reset(); int actualMoveAmountWithinWord = 0; int cursorPos = mCursorPositionWithinWord; - final int[] codePoints; - if (mCodePointSize >= MAX_WORD_LENGTH) { - // If we have more than MAX_WORD_LENGTH characters, we don't have everything inside - // mPrimaryKeyCodes. This should be rare enough that we can afford to just compute - // the array on the fly when this happens. - codePoints = StringUtils.toCodePointArray(mTypedWord.toString()); - } else { - codePoints = mPrimaryKeyCodes; - } + // TODO: Don't make that copy. We can do this directly from mTypedWordCache. + final int[] codePoints = StringUtils.toCodePointArray(mTypedWordCache); if (expectedMoveAmount >= 0) { // Moving the cursor forward for the expected amount or until the end of the word has // been reached, whichever comes first. @@ -261,78 +271,29 @@ public final class WordComposer { final int codePoint = Character.codePointAt(word, i); // We don't want to override the batch input points that are held in mInputPointers // (See {@link #add(int,int,int)}). - add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); - } - } - - /** - * Add a dummy key by retrieving reasonable coordinates - */ - public void addKeyInfo(final int codePoint, final Keyboard keyboard) { - final int x, y; - final Key key; - if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) { - x = key.getX() + key.getWidth() / 2; - y = key.getY() + key.getHeight() / 2; - } else { - x = Constants.NOT_A_COORDINATE; - y = Constants.NOT_A_COORDINATE; + processEvent(Event.createEventForCodePointFromUnknownSource(codePoint)); } - add(codePoint, x, y); } /** * Set the currently composing word to the one passed as an argument. * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity. + * @param codePoints the code points to set as the composing word. + * @param coordinates the x, y coordinates of the key in the CoordinateUtils format + * @param previousWord the previous word, to use as context for suggestions. Can be null if + * the context is nil (typically, at start of text). */ - public void setComposingWord(final CharSequence word, final Keyboard keyboard) { + public void setComposingWord(final int[] codePoints, final int[] coordinates, + final CharSequence previousWord) { reset(); - final int length = word.length(); - for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) { - final int codePoint = Character.codePointAt(word, i); - addKeyInfo(codePoint, keyboard); + final int length = codePoints.length; + for (int i = 0; i < length; ++i) { + processEvent(Event.createEventForCodePointFromAlreadyTypedText(codePoints[i], + CoordinateUtils.xFromArray(coordinates, i), + CoordinateUtils.yFromArray(coordinates, i))); } mIsResumed = true; - } - - /** - * Delete the last keystroke as a result of hitting backspace. - */ - public void deleteLast() { - final int size = size(); - if (size > 0) { - // Note: mTypedWord.length() and mCodes.length differ when there are surrogate pairs - final int stringBuilderLength = mTypedWord.length(); - if (stringBuilderLength < size) { - throw new RuntimeException( - "In WordComposer: mCodes and mTypedWords have non-matching lengths"); - } - final int lastChar = mTypedWord.codePointBefore(stringBuilderLength); - if (Character.isSupplementaryCodePoint(lastChar)) { - mTypedWord.delete(stringBuilderLength - 2, stringBuilderLength); - } else { - mTypedWord.deleteCharAt(stringBuilderLength - 1); - } - if (Character.isUpperCase(lastChar)) mCapsCount--; - if (Character.isDigit(lastChar)) mDigitsCount--; - refreshSize(); - } - // We may have deleted the last one. - if (0 == size()) { - mIsFirstCharCapitalized = false; - } - if (mTrailingSingleQuotesCount > 0) { - --mTrailingSingleQuotesCount; - } else { - int i = mTypedWord.length(); - while (i > 0) { - i = mTypedWord.offsetByCodePoints(i, -1); - if (Constants.CODE_SINGLE_QUOTE != mTypedWord.codePointAt(i)) break; - ++mTrailingSingleQuotesCount; - } - } - mCursorPositionWithinWord = mCodePointSize; - mAutoCorrection = null; + mPreviousWordForSuggestion = null == previousWord ? null : previousWord.toString(); } /** @@ -340,7 +301,11 @@ public final class WordComposer { * @return the word that was typed so far. Never returns null. */ public String getTypedWord() { - return mTypedWord.toString(); + return mTypedWordCache.toString(); + } + + public String getPreviousWordForSuggestion() { + return mPreviousWordForSuggestion; } /** @@ -352,7 +317,12 @@ public final class WordComposer { } public int trailingSingleQuotesCount() { - return mTrailingSingleQuotesCount; + final int lastIndex = mTypedWordCache.length() - 1; + int i = lastIndex; + while (i >= 0 && mTypedWordCache.charAt(i) == Constants.CODE_SINGLE_QUOTE) { + --i; + } + return lastIndex - i; } /** @@ -388,18 +358,21 @@ public final class WordComposer { } /** - * Saves the caps mode at the start of composing. + * Saves the caps mode and the previous word at the start of composing. * - * WordComposer needs to know about this for several reasons. The first is, we need to know - * after the fact what the reason was, to register the correct form into the user history - * dictionary: if the word was automatically capitalized, we should insert it in all-lower - * case but if it's a manual pressing of shift, then it should be inserted as is. + * WordComposer needs to know about the caps mode for several reasons. The first is, we need + * to know after the fact what the reason was, to register the correct form into the user + * history dictionary: if the word was automatically capitalized, we should insert it in + * all-lower case but if it's a manual pressing of shift, then it should be inserted as is. * Also, batch input needs to know about the current caps mode to display correctly * capitalized suggestions. * @param mode the mode at the time of start + * @param previousWord the previous word as context for suggestions. May be null if none. */ - public void setCapitalizedModeAtStartComposingTime(final int mode) { + public void setCapitalizedModeAndPreviousWordAtStartComposingTime(final int mode, + final CharSequence previousWord) { mCapitalizedMode = mode; + mPreviousWordForSuggestion = null == previousWord ? null : previousWord.toString(); } /** @@ -433,15 +406,14 @@ public final class WordComposer { } // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above. - public LastComposedWord commitWord(final int type, final String committedWord, + // committedWord should contain suggestion spans if applicable. + public LastComposedWord commitWord(final int type, final CharSequence committedWord, final String separatorString, final String prevWord) { // 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 int[] primaryKeyCodes = mPrimaryKeyCodes; - mPrimaryKeyCodes = new int[MAX_WORD_LENGTH]; - final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes, - mInputPointers, mTypedWord.toString(), committedWord, separatorString, + final LastComposedWord lastComposedWord = new LastComposedWord(mEvents, + mInputPointers, mTypedWordCache.toString(), committedWord, separatorString, prevWord, mCapitalizedMode); mInputPointers.reset(); if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD @@ -451,12 +423,13 @@ public final class WordComposer { mCapsCount = 0; mDigitsCount = 0; mIsBatchMode = false; - mTypedWord.setLength(0); + mPreviousWordForSuggestion = committedWord.toString(); + mCombinerChain.reset(); + mEvents.clear(); mCodePointSize = 0; - mTrailingSingleQuotesCount = 0; mIsFirstCharCapitalized = false; mCapitalizedMode = CAPS_MODE_OFF; - refreshSize(); + refreshTypedWordCache(); mAutoCorrection = null; mCursorPositionWithinWord = 0; mIsResumed = false; @@ -464,17 +437,26 @@ public final class WordComposer { return lastComposedWord; } - public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) { - mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes; + // Call this when the recorded previous word should be discarded. This is typically called + // when the user inputs a separator that's not whitespace (including the case of the + // double-space-to-period feature). + public void discardPreviousWordForSuggestion() { + mPreviousWordForSuggestion = null; + } + + public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord, + final String previousWord) { + mEvents.clear(); + Collections.copy(mEvents, lastComposedWord.mEvents); mInputPointers.set(lastComposedWord.mInputPointers); - mTypedWord.setLength(0); - mTypedWord.append(lastComposedWord.mTypedWord); - refreshSize(); + mCombinerChain.reset(); + refreshTypedWordCache(); mCapitalizedMode = lastComposedWord.mCapitalizedMode; mAutoCorrection = null; // This will be filled by the next call to updateSuggestion. mCursorPositionWithinWord = mCodePointSize; mRejectedBatchModeSuggestion = null; mIsResumed = true; + mPreviousWordForSuggestion = previousWord; } public boolean isBatchMode() { diff --git a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java index 028f78a87..139e73aa4 100644 --- a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java +++ b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java @@ -26,8 +26,9 @@ import android.os.Environment; import com.android.inputmethod.latin.BinaryDictionaryFileDumper; import com.android.inputmethod.latin.BinaryDictionaryGetter; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; +import com.android.inputmethod.latin.makedict.DictionaryHeader; import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.DialogUtils; import com.android.inputmethod.latin.utils.DictionaryInfoUtils; import com.android.inputmethod.latin.utils.LocaleUtils; @@ -51,7 +52,7 @@ public class ExternalDictionaryGetterForDebug { final File[] files = new File(SOURCE_FOLDER).listFiles(); final ArrayList<String> eligibleList = CollectionUtils.newArrayList(); for (File f : files) { - final FileHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(f); + final DictionaryHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(f); if (null == header) continue; eligibleList.add(f.getName()); } @@ -70,7 +71,7 @@ public class ExternalDictionaryGetterForDebug { } private static void showNoFileDialog(final Context context) { - new AlertDialog.Builder(context) + new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) .setMessage(R.string.read_external_dictionary_no_files_message) .setPositiveButton(android.R.string.ok, new OnClickListener() { @Override @@ -81,8 +82,8 @@ public class ExternalDictionaryGetterForDebug { } private static void showChooseFileDialog(final Context context, final String[] fileNames) { - final AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.read_external_dictionary_multiple_files_title) + new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) + .setTitle(R.string.read_external_dictionary_multiple_files_title) .setItems(fileNames, new OnClickListener() { @Override public void onClick(final DialogInterface dialog, final int which) { @@ -99,7 +100,7 @@ public class ExternalDictionaryGetterForDebug { public static void askInstallFile(final Context context, final String dirPath, final String fileName, final Runnable completeRunnable) { final File file = new File(dirPath, fileName.toString()); - final FileHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file); + final DictionaryHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file); final StringBuilder message = new StringBuilder(); final String locale = header.getLocaleString(); for (String key : header.mDictionaryOptions.mAttributes.keySet()) { @@ -111,7 +112,7 @@ public class ExternalDictionaryGetterForDebug { final String title = String.format( context.getString(R.string.read_external_dictionary_confirm_install_message), languageName); - new AlertDialog.Builder(context) + new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) .setTitle(title) .setMessage(message) .setNegativeButton(android.R.string.cancel, new OnClickListener() { @@ -143,7 +144,7 @@ public class ExternalDictionaryGetterForDebug { } private static void installFile(final Context context, final File file, - final FileHeader header) { + final DictionaryHeader header) { BufferedOutputStream outputStream = null; File tempFile = null; try { @@ -167,7 +168,7 @@ public class ExternalDictionaryGetterForDebug { } } catch (IOException e) { // There was an error: show a dialog - new AlertDialog.Builder(context) + new AlertDialog.Builder(DialogUtils.getPlatformDialogThemeContext(context)) .setTitle(R.string.error) .setMessage(e.toString()) .setPositiveButton(android.R.string.ok, new OnClickListener() { diff --git a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java index dc937fb25..af899c040 100644 --- a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java +++ b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java @@ -29,4 +29,13 @@ public final class ProductionFlag { public static final boolean USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG = false; public static final boolean IS_HARDWARE_KEYBOARD_SUPPORTED = false; + + // When true, enable {@link InputMethodService#onUpdateCursor} callback with + // {@link InputMethodService#setCursorAnchorMonitorMode}, which is not yet available in + // API level 19. Do not turn this on in production until the new API becomes publicly + // available. + public static final boolean USES_CURSOR_ANCHOR_MONITOR = false; + + // Include all suggestions from all dictionaries in {@link SuggestedWords#mRawSuggestions}. + public static final boolean INCLUDE_RAW_SUGGESTIONS = false; } diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java new file mode 100644 index 000000000..f1f906042 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java @@ -0,0 +1,2006 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.inputlogic; + +import android.os.SystemClock; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.SuggestionSpan; +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.EditorInfo; + +import com.android.inputmethod.compat.SuggestionSpanUtils; +import com.android.inputmethod.event.Event; +import com.android.inputmethod.event.InputTransaction; +import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest; +import com.android.inputmethod.latin.InputPointers; +import com.android.inputmethod.latin.LastComposedWord; +import com.android.inputmethod.latin.LatinIME; +import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.RichInputConnection; +import com.android.inputmethod.latin.Suggest; +import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; +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.ProductionFlag; +import com.android.inputmethod.latin.settings.SettingsValues; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; +import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; +import com.android.inputmethod.latin.utils.AsyncResultHolder; +import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.InputTypeUtils; +import com.android.inputmethod.latin.utils.LatinImeLoggerUtils; +import com.android.inputmethod.latin.utils.RecapitalizeStatus; +import com.android.inputmethod.latin.utils.StringUtils; +import com.android.inputmethod.latin.utils.TextRange; +import com.android.inputmethod.research.ResearchLogger; + +import java.util.ArrayList; +import java.util.TreeSet; +import java.util.concurrent.TimeUnit; + +/** + * This class manages the input logic. + */ +public final class InputLogic { + private static final String TAG = InputLogic.class.getSimpleName(); + + // TODO : Remove this member when we can. + private final LatinIME mLatinIME; + private final SuggestionStripViewAccessor mSuggestionStripViewAccessor; + + // Never null. + private InputLogicHandler mInputLogicHandler = InputLogicHandler.NULL_HANDLER; + + // TODO : make all these fields private as soon as possible. + // 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 final Suggest mSuggest = new Suggest(); + + public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; + public final WordComposer mWordComposer; + public final RichInputConnection mConnection; + private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); + + private int mDeleteCount; + private long mLastKeyTime; + public final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet(); + + // Keeps track of most recently inserted text (multi-character key) for reverting + private String mEnteredText; + + // TODO: This boolean is persistent state and causes large side effects at unexpected times. + // Find a way to remove it for readability. + private boolean mIsAutoCorrectionIndicatorOn; + private long mDoubleSpacePeriodCountdownStart; + + public InputLogic(final LatinIME latinIME, + final SuggestionStripViewAccessor suggestionStripViewAccessor) { + mLatinIME = latinIME; + mSuggestionStripViewAccessor = suggestionStripViewAccessor; + mWordComposer = new WordComposer(); + mConnection = new RichInputConnection(latinIME); + mInputLogicHandler = InputLogicHandler.NULL_HANDLER; + } + + /** + * Initializes the input logic for input in an editor. + * + * Call this when input starts or restarts in some editor (typically, in onStartInputView). + * If the input is starting in the same field as before, set `restarting' to true. This allows + * the input logic to reset only necessary stuff and save performance. Also, when restarting + * some things must not be done (for example, the keyboard should not be reset to the + * alphabetic layout), so do not send false to this just in case. + * + * @param restarting whether input is starting in the same field as before. Unused for now. + * @param editorInfo the editorInfo associated with the editor. + */ + public void startInput(final boolean restarting, final EditorInfo editorInfo) { + mEnteredText = null; + resetComposingState(true /* alsoResetLastComposedWord */); + mDeleteCount = 0; + mSpaceState = SpaceState.NONE; + mRecapitalizeStatus.deactivate(); + mCurrentlyPressedHardwareKeys.clear(); + mSuggestedWords = SuggestedWords.EMPTY; + // 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(); + cancelDoubleSpacePeriodCountdown(); + if (InputLogicHandler.NULL_HANDLER == mInputLogicHandler) { + mInputLogicHandler = new InputLogicHandler(mLatinIME, this); + } else { + mInputLogicHandler.reset(); + } + } + + /** + * Clean up the input logic after input is finished. + */ + public void finishInput() { + if (mWordComposer.isComposingWord()) { + mConnection.finishComposingText(); + } + resetComposingState(true /* alsoResetLastComposedWord */); + mInputLogicHandler.reset(); + } + + // Normally this class just gets out of scope after the process ends, but in unit tests, we + // create several instances of LatinIME in the same process, which results in several + // instances of InputLogic. This cleans up the associated handler so that tests don't leak + // handlers. + public void recycle() { + final InputLogicHandler inputLogicHandler = mInputLogicHandler; + mInputLogicHandler = InputLogicHandler.NULL_HANDLER; + inputLogicHandler.destroy(); + mSuggest.mDictionaryFacilitator.closeDictionaries(); + } + + /** + * React to a string input. + * + * This is triggered by keys that input many characters at once, like the ".com" key or + * some additional keys for example. + * + * @param settingsValues the current values of the settings. + * @param event the input event containing the data. + */ + public void onTextInput(final SettingsValues settingsValues, final Event event, + // TODO: remove this argument + final LatinIME.UIHandler handler) { + final String rawText = event.mText.toString(); + mConnection.beginBatchEdit(); + if (mWordComposer.isComposingWord()) { + commitCurrentAutoCorrection(settingsValues, rawText, handler); + } else { + resetComposingState(true /* alsoResetLastComposedWord */); + } + handler.postUpdateSuggestionStrip(); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS + && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) { + ResearchLogger.getInstance().onResearchKeySelected(mLatinIME); + return; + } + final String text = performSpecificTldProcessingOnTextInput(rawText); + if (SpaceState.PHANTOM == mSpaceState) { + promotePhantomSpace(settingsValues); + } + mConnection.commitText(text, 1); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */); + } + mConnection.endBatchEdit(); + // Space state must be updated before calling updateShiftState + mSpaceState = SpaceState.NONE; + mEnteredText = text; + } + + /** + * A suggestion was picked from the suggestion strip. + * @param settingsValues the current values of the settings. + * @param index the index of the suggestion. + * @param suggestionInfo the suggestion info. + * @param keyboardShiftState the shift state of the keyboard, as returned by + * {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()} + * @return the complete transaction object + */ + // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} + // interface + public InputTransaction onPickSuggestionManually(final SettingsValues settingsValues, + final int index, final SuggestedWordInfo suggestionInfo, final int keyboardShiftState, + // TODO: remove this argument + final LatinIME.UIHandler handler) { + final SuggestedWords suggestedWords = mSuggestedWords; + final String suggestion = suggestionInfo.mWord; + // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput + if (suggestion.length() == 1 && suggestedWords.isPunctuationSuggestions()) { + // Word separators are suggested before the user inputs something. + // So, LatinImeLogger logs "" as a user's input. + LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords); + // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. + final Event event = Event.createPunctuationSuggestionPickedEvent(suggestionInfo); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, + false /* isBatchMode */, suggestedWords.mIsPrediction); + } + return onCodeInput(settingsValues, event, keyboardShiftState, handler); + } + + final Event event = Event.createSuggestionPickedEvent(suggestionInfo); + final InputTransaction inputTransaction = new InputTransaction(settingsValues, + event, SystemClock.uptimeMillis(), mSpaceState, keyboardShiftState); + mConnection.beginBatchEdit(); + if (SpaceState.PHANTOM == mSpaceState && suggestion.length() > 0 + // In the batch input mode, a manually picked suggested word should just replace + // the current batch input text and there is no need for a phantom space. + && !mWordComposer.isBatchMode()) { + final int firstChar = Character.codePointAt(suggestion, 0); + if (!settingsValues.isWordSeparator(firstChar) + || settingsValues.isUsuallyPrecededBySpace(firstChar)) { + promotePhantomSpace(settingsValues); + } + } + + // TODO: We should not need the following branch. We should be able to take the same + // code path as for other kinds, use commitChosenWord, and do everything normally. We will + // 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 (SuggestedWordInfo.KIND_APP_DEFINED == suggestionInfo.mKind) { + mSuggestedWords = SuggestedWords.EMPTY; + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + resetComposingState(true /* alsoResetLastComposedWord */); + mConnection.commitCompletion(suggestionInfo.mApplicationSpecifiedCompletionInfo); + mConnection.endBatchEdit(); + return inputTransaction; + } + + // We need to log before we commit, because the word composer will store away the user + // typed word. + final String replacedWord = mWordComposer.getTypedWord(); + LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); + commitChosenWord(settingsValues, suggestion, + LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, + mWordComposer.isBatchMode(), suggestionInfo.mScore, + suggestionInfo.mKind, suggestionInfo.mSourceDict.mDictType); + } + mConnection.endBatchEdit(); + // Don't allow cancellation of manual pick + mLastComposedWord.deactivate(); + // Space state must be updated before calling updateShiftState + mSpaceState = SpaceState.PHANTOM; + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + + // We should show the "Touch again to save" hint if the user pressed the first entry + // AND it's in none of our current dictionaries (main, user or otherwise). + final DictionaryFacilitatorForSuggest dictionaryFacilitator = + mSuggest.mDictionaryFacilitator; + final boolean showingAddToDictionaryHint = + (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind + || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind) + && !dictionaryFacilitator.isValidWord(suggestion, true /* ignoreCase */); + + if (settingsValues.mIsInternal) { + LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } + if (showingAddToDictionaryHint && dictionaryFacilitator.isUserDictionaryEnabled()) { + mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion); + } else { + // If we're not showing the "Touch again to save", then update the suggestion strip. + handler.postUpdateSuggestionStrip(); + } + return inputTransaction; + } + + /** + * Consider an update to the cursor position. Evaluate whether this update has happened as + * part of normal typing or whether it was an explicit cursor move by the user. In any case, + * do the necessary adjustments. + * @param oldSelStart old selection start + * @param oldSelEnd old selection end + * @param newSelStart new selection start + * @param newSelEnd new selection end + * @return whether the cursor has moved as a result of user interaction. + */ + public boolean onUpdateSelection(final int oldSelStart, final int oldSelEnd, + final int newSelStart, final int newSelEnd) { + if (mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, oldSelEnd, newSelEnd)) { + return false; + } + // TODO: the following is probably better done in resetEntireInputState(). + // it should only happen when the cursor moved, and the very purpose of the + // test below is to narrow down whether this happened or not. Likewise with + // the call to updateShiftState. + // We set this to NONE because after a cursor move, we don't want the space + // state-related special processing to kick in. + mSpaceState = SpaceState.NONE; + + final boolean selectionChangedOrSafeToReset = + oldSelStart != newSelStart || oldSelEnd != newSelEnd // selection changed + || !mWordComposer.isComposingWord(); // safe to reset + final boolean hasOrHadSelection = (oldSelStart != oldSelEnd || newSelStart != newSelEnd); + final int moveAmount = newSelStart - oldSelStart; + if (selectionChangedOrSafeToReset && (hasOrHadSelection + || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { + // If we are composing a word and moving the cursor, we would want to set a + // suggestion span for recorrection to work correctly. Unfortunately, that + // would involve the keyboard committing some new text, which would move the + // cursor back to where it was. Latin IME could then fix the position of the cursor + // again, but the asynchronous nature of the calls results in this wreaking havoc + // with selection on double tap and the like. + // Another option would be to send suggestions each time we set the composing + // text, but that is probably too expensive to do, so we decided to leave things + // as is. + // Also, we're posting a resume suggestions message, and this will update the + // suggestions strip in a few milliseconds, so if we cleared the suggestion strip here + // we'd have the suggestion strip noticeably janky. To avoid that, we don't clear + // it here, which means we'll keep outdated suggestions for a split second but the + // visual result is better. + resetEntireInputState(newSelStart, newSelEnd, false /* clearSuggestionStrip */); + } else { + // resetEntireInputState calls resetCachesUponCursorMove, but forcing the + // composition to end. But in all cases where we don't reset the entire input + // state, we still want to tell the rich input connection about the new cursor + // position so that it can update its caches. + mConnection.resetCachesUponCursorMoveAndReturnSuccess( + newSelStart, newSelEnd, false /* shouldFinishComposition */); + } + + // We moved the cursor. If we are touching a word, we need to resume suggestion. + mLatinIME.mHandler.postResumeSuggestions(); + // Reset the last recapitalization. + mRecapitalizeStatus.deactivate(); + return true; + } + + /** + * React to a code input. It may be a code point to insert, or a symbolic value that influences + * the keyboard behavior. + * + * Typically, this is called whenever a key is pressed on the software keyboard. This is not + * the entry point for gesture input; see the onBatchInput* family of functions for this. + * + * @param settingsValues the current settings values. + * @param event the event to handle. + * @param keyboardShiftMode the current shift mode of the keyboard, as returned by + * {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()} + * @return the complete transaction object + */ + public InputTransaction onCodeInput(final SettingsValues settingsValues, final Event event, + final int keyboardShiftMode, + // TODO: remove this argument + final LatinIME.UIHandler handler) { + // TODO: rework the following to not squash the keycode and the code point into the same + // var because it's confusing. Instead the switch() should handle this in a readable manner. + final int code = + Event.NOT_A_CODE_POINT == event.mCodePoint ? event.mKeyCode : event.mCodePoint; + final InputTransaction inputTransaction = new InputTransaction(settingsValues, event, + SystemClock.uptimeMillis(), mSpaceState, + getActualCapsMode(settingsValues, keyboardShiftMode)); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_onCodeInput(code, event.mX, event.mY); + } + if (event.mKeyCode != Constants.CODE_DELETE + || inputTransaction.mTimestamp > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) { + mDeleteCount = 0; + } + mLastKeyTime = inputTransaction.mTimestamp; + mConnection.beginBatchEdit(); + if (!mWordComposer.isComposingWord()) { + mIsAutoCorrectionIndicatorOn = false; + } + + // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state. + if (event.mCodePoint != Constants.CODE_SPACE) { + cancelDoubleSpacePeriodCountdown(); + } + + boolean didAutoCorrect = false; + if (Event.NOT_A_KEY_CODE != event.mKeyCode) { + // A special key, like delete, shift, emoji, or the settings key. + switch (event.mKeyCode) { + case Constants.CODE_DELETE: + handleBackspace(inputTransaction); + LatinImeLogger.logOnDelete(event.mX, event.mY); + break; + case Constants.CODE_SHIFT: + performRecapitalization(inputTransaction.mSettingsValues); + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + break; + case Constants.CODE_CAPSLOCK: + // Note: Changing keyboard to shift lock state is handled in + // {@link KeyboardSwitcher#onCodeInput(int)}. + break; + case Constants.CODE_SYMBOL_SHIFT: + // Note: Calling back to the keyboard on the symbol Shift key is handled in + // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. + break; + case Constants.CODE_SWITCH_ALPHA_SYMBOL: + // Note: Calling back to the keyboard on symbol key is handled in + // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. + break; + case Constants.CODE_SETTINGS: + onSettingsKeyPressed(); + break; + case Constants.CODE_SHORTCUT: + // We need to switch to the shortcut IME. This is handled by LatinIME since the + // input logic has no business with IME switching. + break; + case Constants.CODE_ACTION_NEXT: + performEditorAction(EditorInfo.IME_ACTION_NEXT); + break; + case Constants.CODE_ACTION_PREVIOUS: + performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); + break; + case Constants.CODE_LANGUAGE_SWITCH: + handleLanguageSwitchKey(); + break; + case Constants.CODE_EMOJI: + // Note: Switching emoji keyboard is being handled in + // {@link KeyboardState#onCodeInput(int,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)}. + break; + case Constants.CODE_SHIFT_ENTER: + // TODO: remove this object + final InputTransaction tmpTransaction = new InputTransaction( + inputTransaction.mSettingsValues, inputTransaction.mEvent, + inputTransaction.mTimestamp, inputTransaction.mSpaceState, + inputTransaction.mShiftState); + didAutoCorrect = handleNonSpecialCharacter(tmpTransaction, handler); + break; + default: + throw new RuntimeException("Unknown key code : " + event.mKeyCode); + } + } else { + switch (event.mCodePoint) { + case Constants.CODE_ENTER: + final EditorInfo editorInfo = getCurrentInputEditorInfo(); + final int imeOptionsActionId = + InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); + if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { + // Either we have an actionLabel and we should performEditorAction with + // actionId regardless of its value. + performEditorAction(editorInfo.actionId); + } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { + // We didn't have an actionLabel, but we had another action to execute. + // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, + // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it + // means there should be an action and the app didn't bother to set a specific + // code for it - presumably it only handles one. It does not have to be treated + // in any specific way: anything that is not IME_ACTION_NONE should be sent to + // performEditorAction. + performEditorAction(imeOptionsActionId); + } else { + // No action label, and the action from imeOptions is NONE: this is a regular + // enter key that should input a carriage return. + didAutoCorrect = handleNonSpecialCharacter(inputTransaction, handler); + } + break; + default: + didAutoCorrect = handleNonSpecialCharacter(inputTransaction, handler); + break; + } + } + if (!didAutoCorrect && event.mKeyCode != Constants.CODE_SHIFT + && event.mKeyCode != Constants.CODE_CAPSLOCK + && event.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) + mLastComposedWord.deactivate(); + if (Constants.CODE_DELETE != event.mKeyCode) { + mEnteredText = null; + } + mConnection.endBatchEdit(); + return inputTransaction; + } + + public void onStartBatchInput(final SettingsValues settingsValues, + // TODO: remove these arguments + final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { + mInputLogicHandler.onStartBatchInput(); + handler.showGesturePreviewAndSuggestionStrip( + SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); + handler.cancelUpdateSuggestionStrip(); + ++mAutoCommitSequenceNumber; + mConnection.beginBatchEdit(); + if (mWordComposer.isComposingWord()) { + if (settingsValues.mIsInternal) { + if (mWordComposer.isBatchMode()) { + LatinImeLoggerUtils.onAutoCorrection("", mWordComposer.getTypedWord(), " ", + mWordComposer); + } + } + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the batch input at the current cursor position. + resetEntireInputState(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); + } else if (mWordComposer.isSingleLetter()) { + // We auto-correct the previous (typed, not gestured) string iff it's one character + // long. The reason for this is, even in the middle of gesture typing, you'll still + // tap one-letter words and you want them auto-corrected (typically, "i" in English + // should become "I"). However for any longer word, we assume that the reason for + // tapping probably is that the word you intend to type is not in the dictionary, + // so we do not attempt to correct, on the assumption that if that was a dictionary + // word, the user would probably have gestured instead. + commitCurrentAutoCorrection(settingsValues, LastComposedWord.NOT_A_SEPARATOR, + handler); + } else { + commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR); + } + } + final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); + if (Character.isLetterOrDigit(codePointBeforeCursor) + || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) { + final boolean autoShiftHasBeenOverriden = keyboardSwitcher.getKeyboardShiftMode() != + getCurrentAutoCapsState(settingsValues); + mSpaceState = SpaceState.PHANTOM; + if (!autoShiftHasBeenOverriden) { + // When we change the space state, we need to update the shift state of the + // keyboard unless it has been overridden manually. This is happening for example + // after typing some letters and a period, then gesturing; the keyboard is not in + // caps mode yet, but since a gesture is starting, it should go in caps mode, + // unless the user explictly said it should not. + keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues), + getCurrentRecapitalizeState()); + } + } + mConnection.endBatchEdit(); + mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( + getActualCapsMode(settingsValues, keyboardSwitcher.getKeyboardShiftMode()), + // Prev word is 1st word before cursor + getNthPreviousWordForSuggestion( + settingsValues.mSpacingAndPunctuations, 1 /* nthPreviousWord */)); + } + + /* The sequence number member is only used in onUpdateBatchInput. It is increased each time + * auto-commit happens. The reason we need this is, when auto-commit happens we trim the + * input pointers that are held in a singleton, and to know how much to trim we rely on the + * results of the suggestion process that is held in mSuggestedWords. + * However, the suggestion process is asynchronous, and sometimes we may enter the + * onUpdateBatchInput method twice without having recomputed suggestions yet, or having + * received new suggestions generated from not-yet-trimmed input pointers. In this case, the + * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we + * remove an unrelated number of pointers (possibly even more than are left in the input + * pointers, leading to a crash). + * To avoid that, we increase the sequence number each time we auto-commit and trim the + * input pointers, and we do not use any suggested words that have been generated with an + * earlier sequence number. + */ + private int mAutoCommitSequenceNumber = 1; + public void onUpdateBatchInput(final SettingsValues settingsValues, + final InputPointers batchPointers, + // TODO: remove these arguments + final KeyboardSwitcher keyboardSwitcher) { + if (settingsValues.mPhraseGestureEnabled) { + final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate(); + // If these suggested words have been generated with out of date input pointers, then + // we skip auto-commit (see comments above on the mSequenceNumber member). + if (null != candidate + && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) { + if (candidate.mSourceDict.shouldAutoCommit(candidate)) { + final String[] commitParts = candidate.mWord.split(" ", 2); + batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); + promotePhantomSpace(settingsValues); + mConnection.commitText(commitParts[0], 0); + mSpaceState = SpaceState.PHANTOM; + keyboardSwitcher.requestUpdatingShiftState( + getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState()); + mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( + getActualCapsMode(settingsValues, + keyboardSwitcher.getKeyboardShiftMode()), commitParts[0]); + ++mAutoCommitSequenceNumber; + } + } + } + mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber); + } + + public void onEndBatchInput(final InputPointers batchPointers) { + mInputLogicHandler.updateTailBatchInput(batchPointers, mAutoCommitSequenceNumber); + ++mAutoCommitSequenceNumber; + } + + // TODO: remove this argument + public void onCancelBatchInput(final LatinIME.UIHandler handler) { + mInputLogicHandler.onCancelBatchInput(); + handler.showGesturePreviewAndSuggestionStrip( + SuggestedWords.EMPTY, 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) { + mSuggestedWords = suggestedWords; + final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; + // Put a blue underline to a word in TextView which will be auto-corrected. + if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator + && mWordComposer.isComposingWord()) { + mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; + final CharSequence textWithUnderline = + getTextWithUnderline(mWordComposer.getTypedWord()); + // TODO: when called from an updateSuggestionStrip() call that results from a posted + // message, this is called outside any batch edit. Potentially, this may result in some + // janky flickering of the screen, although the display speed makes it unlikely in + // the practice. + mConnection.setComposingText(textWithUnderline, 1); + } + } + + /** + * Handle inputting a code point to the editor. + * + * Non-special keys are those that generate a single code point. + * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that + * manage keyboard-related stuff like shift, language switch, settings, layout switch, or + * any key that results in multiple code points like the ".com" key. + * + * @param inputTransaction The transaction in progress. + * @return whether this caused an auto-correction to happen. + */ + private boolean handleNonSpecialCharacter(final InputTransaction inputTransaction, + // TODO: remove this argument + final LatinIME.UIHandler handler) { + final int codePoint = inputTransaction.mEvent.mCodePoint; + mSpaceState = SpaceState.NONE; + final boolean didAutoCorrect; + if (inputTransaction.mSettingsValues.isWordSeparator(codePoint) + || Character.getType(codePoint) == Character.OTHER_SYMBOL) { + didAutoCorrect = handleSeparator(inputTransaction, + inputTransaction.mEvent.isSuggestionStripPress(), handler); + if (inputTransaction.mSettingsValues.mIsInternal) { + LatinImeLoggerUtils.onSeparator((char)codePoint, + inputTransaction.mEvent.mX, inputTransaction.mEvent.mY); + } + } else { + didAutoCorrect = false; + if (SpaceState.PHANTOM == inputTransaction.mSpaceState) { + if (inputTransaction.mSettingsValues.mIsInternal) { + if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) { + LatinImeLoggerUtils.onAutoCorrection("", mWordComposer.getTypedWord(), " ", + mWordComposer); + } + } + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the character at the current cursor position. + resetEntireInputState(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); + } else { + commitTyped(inputTransaction.mSettingsValues, LastComposedWord.NOT_A_SEPARATOR); + } + } + handleNonSeparator(inputTransaction.mSettingsValues, inputTransaction); + } + return didAutoCorrect; + } + + /** + * Handle a non-separator. + * @param settingsValues The current settings values. + * @param inputTransaction The transaction in progress. + */ + private void handleNonSeparator(final SettingsValues settingsValues, + final InputTransaction inputTransaction) { + final int codePoint = inputTransaction.mEvent.mCodePoint; + // TODO: refactor this method to stop flipping isComposingWord around all the time, and + // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter + // which has the same name as other handle* methods but is not the same. + boolean isComposingWord = mWordComposer.isComposingWord(); + + // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. + // See onStartBatchInput() to see how to do it. + if (SpaceState.PHANTOM == inputTransaction.mSpaceState + && !settingsValues.isWordConnector(codePoint)) { + if (isComposingWord) { + // Sanity check + throw new RuntimeException("Should not be composing here"); + } + promotePhantomSpace(settingsValues); + } + + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the character at the current cursor position. + resetEntireInputState(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); + isComposingWord = false; + } + // We want to find out whether to start composing a new word with this character. If so, + // we need to reset the composing state and switch isComposingWord. The order of the + // tests is important for good performance. + // We only start composing if we're not already composing. + if (!isComposingWord + // We only start composing if this is a word code point. Essentially that means it's a + // a letter or a word connector. + && settingsValues.isWordCodePoint(codePoint) + // We never go into composing state if suggestions are not requested. + && settingsValues.isSuggestionsRequested() && + // In languages with spaces, we only start composing a word when we are not already + // touching a word. In languages without spaces, the above conditions are sufficient. + (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations) + || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) { + // Reset entirely the composing state anyway, then start composing a new word unless + // the character is a single quote or a dash. The idea here is, single quote and dash + // are not separators and they should be treated as normal characters, except in the + // first position where they should not start composing a word. + isComposingWord = (Constants.CODE_SINGLE_QUOTE != codePoint + && Constants.CODE_DASH != codePoint); + // Here we don't need to reset the last composed word. It will be reset + // when we commit this one, if we ever do; if on the other hand we backspace + // it entirely and resume suggestions on the previous word, we'd like to still + // have touch coordinates for it. + resetComposingState(false /* alsoResetLastComposedWord */); + } + if (isComposingWord) { + mWordComposer.processEvent(inputTransaction.mEvent); + // If it's the first letter, make note of auto-caps state + if (mWordComposer.isSingleLetter()) { + // We pass 1 to getPreviousWordForSuggestion because we were not composing a word + // yet, so the word we want is the 1st word before the cursor. + mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( + inputTransaction.mShiftState, getNthPreviousWordForSuggestion( + settingsValues.mSpacingAndPunctuations, 1 /* nthPreviousWord */)); + } + mConnection.setComposingText(getTextWithUnderline( + mWordComposer.getTypedWord()), 1); + } else { + final boolean swapWeakSpace = maybeStripSpace(inputTransaction, + inputTransaction.mEvent.isSuggestionStripPress()); + + sendKeyCodePoint(settingsValues, codePoint); + + if (swapWeakSpace) { + swapSwapperAndSpace(inputTransaction); + mSpaceState = SpaceState.WEAK; + } + // In case the "add to dictionary" hint was still displayed. + mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); + } + inputTransaction.setRequiresUpdateSuggestions(); + if (settingsValues.mIsInternal) { + LatinImeLoggerUtils.onNonSeparator((char)codePoint, inputTransaction.mEvent.mX, + inputTransaction.mEvent.mY); + } + } + + /** + * Handle input of a separator code point. + * @param inputTransaction The transaction in progress. + * @param isFromSuggestionStrip whether this code point comes from the suggestion strip. + * @return whether this caused an auto-correction to happen. + */ + private boolean handleSeparator(final InputTransaction inputTransaction, + final boolean isFromSuggestionStrip, + // TODO: remove this argument + final LatinIME.UIHandler handler) { + final int codePoint = inputTransaction.mEvent.mCodePoint; + boolean didAutoCorrect = false; + // We avoid sending spaces in languages without spaces if we were composing. + final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint + && !inputTransaction.mSettingsValues.mSpacingAndPunctuations + .mCurrentLanguageHasSpaces + && mWordComposer.isComposingWord(); + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can insert the separator at the current cursor position. + resetEntireInputState(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); + } + // isComposingWord() may have changed since we stored wasComposing + if (mWordComposer.isComposingWord()) { + if (inputTransaction.mSettingsValues.mCorrectionEnabled) { + final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR + : StringUtils.newSingleCodePointString(codePoint); + commitCurrentAutoCorrection(inputTransaction.mSettingsValues, separator, handler); + didAutoCorrect = true; + } else { + commitTyped(inputTransaction.mSettingsValues, + StringUtils.newSingleCodePointString(codePoint)); + } + } + + final boolean swapWeakSpace = maybeStripSpace(inputTransaction, isFromSuggestionStrip); + + final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint + && mConnection.isInsideDoubleQuoteOrAfterDigit(); + + final boolean needsPrecedingSpace; + if (SpaceState.PHANTOM != inputTransaction.mSpaceState) { + needsPrecedingSpace = false; + } else if (Constants.CODE_DOUBLE_QUOTE == codePoint) { + // Double quotes behave like they are usually preceded by space iff we are + // not inside a double quote or after a digit. + needsPrecedingSpace = !isInsideDoubleQuoteOrAfterDigit; + } else { + needsPrecedingSpace = inputTransaction.mSettingsValues.isUsuallyPrecededBySpace( + codePoint); + } + + if (needsPrecedingSpace) { + promotePhantomSpace(inputTransaction.mSettingsValues); + } + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_handleSeparator(codePoint, mWordComposer.isComposingWord()); + } + + if (!shouldAvoidSendingCode) { + sendKeyCodePoint(inputTransaction.mSettingsValues, codePoint); + } + + if (Constants.CODE_SPACE == codePoint) { + if (maybeDoubleSpacePeriod(inputTransaction)) { + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + mSpaceState = SpaceState.DOUBLE; + } else if (!mSuggestedWords.isPunctuationSuggestions()) { + mSpaceState = SpaceState.WEAK; + } + + startDoubleSpacePeriodCountdown(inputTransaction); + inputTransaction.setRequiresUpdateSuggestions(); + } else { + if (swapWeakSpace) { + swapSwapperAndSpace(inputTransaction); + mSpaceState = SpaceState.SWAP_PUNCTUATION; + } else if ((SpaceState.PHANTOM == inputTransaction.mSpaceState + && inputTransaction.mSettingsValues.isUsuallyFollowedBySpace(codePoint)) + || (Constants.CODE_DOUBLE_QUOTE == codePoint + && isInsideDoubleQuoteOrAfterDigit)) { + // If we are in phantom space state, and the user presses a separator, we want to + // stay in phantom space state so that the next keypress has a chance to add the + // space. For example, if I type "Good dat", pick "day" from the suggestion strip + // then insert a comma and go on to typing the next word, I want the space to be + // inserted automatically before the next word, the same way it is when I don't + // input the comma. A double quote behaves like it's usually followed by space if + // we're inside a double quote. + // The case is a little different if the separator is a space stripper. Such a + // separator does not normally need a space on the right (that's the difference + // between swappers and strippers), so we should not stay in phantom space state if + // the separator is a stripper. Hence the additional test above. + mSpaceState = SpaceState.PHANTOM; + } + + // Set punctuation right away. onUpdateSelection will fire but tests whether it is + // already displayed or not, so it's okay. + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + } + + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + return didAutoCorrect; + } + + /** + * Handle a press on the backspace key. + * @param inputTransaction The transaction in progress. + */ + private void handleBackspace(final InputTransaction inputTransaction) { + mSpaceState = SpaceState.NONE; + mDeleteCount++; + + // In many cases after backspace, we need to update the shift state. Normally we need + // to do this right away to avoid the shift state being out of date in case the user types + // backspace then some other character very fast. However, in the case of backspace key + // repeat, this can lead to flashiness when the cursor flies over positions where the + // shift state should be updated, so if this is a key repeat, we update after a small delay. + // Then again, even in the case of a key repeat, if the cursor is at start of text, it + // can't go any further back, so we can update right away even if it's a key repeat. + final int shiftUpdateKind = + inputTransaction.mEvent.isKeyRepeat() && mConnection.getExpectedSelectionStart() > 0 + ? InputTransaction.SHIFT_UPDATE_LATER : InputTransaction.SHIFT_UPDATE_NOW; + inputTransaction.requireShiftUpdate(shiftUpdateKind); + + if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { + // If we are in the middle of a recorrection, we need to commit the recorrection + // first so that we can remove the character at the current cursor position. + resetEntireInputState(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); + // When we exit this if-clause, mWordComposer.isComposingWord() will return false. + } + if (mWordComposer.isComposingWord()) { + if (mWordComposer.isBatchMode()) { + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + final String word = mWordComposer.getTypedWord(); + ResearchLogger.latinIME_handleBackspace_batch(word, 1); + } + final String rejectedSuggestion = mWordComposer.getTypedWord(); + mWordComposer.reset(); + mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); + } else { + mWordComposer.processEvent(inputTransaction.mEvent); + } + mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); + inputTransaction.setRequiresUpdateSuggestions(); + } else { + if (mLastComposedWord.canRevertCommit()) { + if (inputTransaction.mSettingsValues.mIsInternal) { + LatinImeLoggerUtils.onAutoCorrectionCancellation(); + } + revertCommit(inputTransaction); + return; + } + if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { + // Cancel multi-character input: remove the text we just entered. + // 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); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText); + } + 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 + // reverting any autocorrect at this point. So we can safely return. + return; + } + if (SpaceState.DOUBLE == inputTransaction.mSpaceState) { + cancelDoubleSpacePeriodCountdown(); + if (mConnection.revertDoubleSpacePeriod()) { + // No need to reset mSpaceState, it has already be done (that's why we + // receive it as a parameter) + return; + } + } else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { + if (mConnection.revertSwapPunctuation()) { + // Likewise + return; + } + } + + // No cancelling of commit/double space/swap: we have a regular backspace. + // We should backspace one char and restart suggestion if at the end of a word. + if (mConnection.hasSelection()) { + // If there is a selection, remove it. + final int numCharsDeleted = mConnection.getExpectedSelectionEnd() + - mConnection.getExpectedSelectionStart(); + mConnection.setSelection(mConnection.getExpectedSelectionEnd(), + mConnection.getExpectedSelectionEnd()); + mConnection.deleteSurroundingText(numCharsDeleted, 0); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_handleBackspace(numCharsDeleted, + false /* shouldUncommitLogUnit */); + } + } 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 + // 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. + sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { + sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); + } + } else { + final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); + if (codePointBeforeCursor == Constants.NOT_A_CODE) { + // HACK for backward compatibility with broken apps that haven't realized + // yet that hardware keyboards are not the only way of inputting text. + // Nothing to delete before the cursor. We should not do anything, but many + // broken apps expect something to happen in this case so that they can + // 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); + return; + } + final int lengthToDelete = + Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; + mConnection.deleteSurroundingText(lengthToDelete, 0); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_handleBackspace(lengthToDelete, + true /* shouldUncommitLogUnit */); + } + if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { + final int codePointBeforeCursorToDeleteAgain = + mConnection.getCodePointBeforeCursor(); + if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { + final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( + codePointBeforeCursorToDeleteAgain) ? 2 : 1; + mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain, + true /* shouldUncommitLogUnit */); + } + } + } + } + } + if (inputTransaction.mSettingsValues.isSuggestionStripVisible() + && inputTransaction.mSettingsValues.mSpacingAndPunctuations + .mCurrentLanguageHasSpaces + && !mConnection.isCursorFollowedByWordCharacter( + inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { + restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues, + true /* includeResumedWordInSuggestions */); + } + } + } + + /** + * Handle a press on the language switch key (the "globe key") + */ + private void handleLanguageSwitchKey() { + mLatinIME.switchToNextSubtype(); + } + + /** + * Swap a space with a space-swapping punctuation sign. + * + * This method will check that there are two characters before the cursor and that the first + * one is a space before it does the actual swapping. + * @param inputTransaction The transaction in progress. + */ + private void swapSwapperAndSpace(final InputTransaction inputTransaction) { + final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0); + // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. + if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Constants.CODE_SPACE) { + mConnection.deleteSurroundingText(2, 0); + final String text = lastTwo.charAt(1) + " "; + mConnection.commitText(text, 1); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text); + } + inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); + } + } + + /* + * Strip a trailing space if necessary and returns whether it's a swap weak space situation. + * @param inputTransaction The transaction in progress. + * @param isFromSuggestionStrip Whether this code point is coming from the suggestion strip. + * @return whether we should swap the space instead of removing it. + */ + private boolean maybeStripSpace(final InputTransaction inputTransaction, + final boolean isFromSuggestionStrip) { + final int codePoint = inputTransaction.mEvent.mCodePoint; + if (Constants.CODE_ENTER == codePoint && + SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { + mConnection.removeTrailingSpace(); + return false; + } + if ((SpaceState.WEAK == inputTransaction.mSpaceState + || SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) + && isFromSuggestionStrip) { + if (inputTransaction.mSettingsValues.isUsuallyPrecededBySpace(codePoint)) { + return false; + } + if (inputTransaction.mSettingsValues.isUsuallyFollowedBySpace(codePoint)) { + return true; + } + mConnection.removeTrailingSpace(); + } + return false; + } + + public void startDoubleSpacePeriodCountdown(final InputTransaction inputTransaction) { + mDoubleSpacePeriodCountdownStart = inputTransaction.mTimestamp; + } + + public void cancelDoubleSpacePeriodCountdown() { + mDoubleSpacePeriodCountdownStart = 0; + } + + public boolean isDoubleSpacePeriodCountdownActive(final InputTransaction inputTransaction) { + return inputTransaction.mTimestamp - mDoubleSpacePeriodCountdownStart + < inputTransaction.mSettingsValues.mDoubleSpacePeriodTimeout; + } + + /** + * Apply the double-space-to-period transformation if applicable. + * + * The double-space-to-period transformation means that we replace two spaces with a + * period-space sequence of characters. This typically happens when the user presses space + * twice in a row quickly. + * This method will check that the double-space-to-period is active in settings, that the + * two spaces have been input close enough together, and that the previous character allows + * for the transformation to take place. If all of these conditions are fulfilled, this + * method applies the transformation and returns true. Otherwise, it does nothing and + * returns false. + * + * @param inputTransaction The transaction in progress. + * @return true if we applied the double-space-to-period transformation, false otherwise. + */ + private boolean maybeDoubleSpacePeriod(final InputTransaction inputTransaction) { + if (!inputTransaction.mSettingsValues.mUseDoubleSpacePeriod) return false; + if (!isDoubleSpacePeriodCountdownActive(inputTransaction)) return false; + // We only do this when we see two spaces and an accepted code point before the cursor. + // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars. + final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0); + if (null == lastThree) return false; + final int length = lastThree.length(); + if (length < 3) return false; + if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false; + if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false; + // We know there are spaces in pos -1 and -2, and we have at least three chars. + // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space, + // so this is fine. + final int firstCodePoint = + Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ? + Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3); + if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) { + cancelDoubleSpacePeriodCountdown(); + mConnection.deleteSurroundingText(2, 0); + final String textToInsert = inputTransaction.mSettingsValues.mSpacingAndPunctuations + .mSentenceSeparatorAndSpace; + mConnection.commitText(textToInsert, 1); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert, + false /* isBatchMode */); + } + mWordComposer.discardPreviousWordForSuggestion(); + return true; + } + return false; + } + + /** + * Returns whether this code point can be followed by the double-space-to-period transformation. + * + * See #maybeDoubleSpaceToPeriod for details. + * Generally, most word characters can be followed by the double-space-to-period transformation, + * while most punctuation can't. Some punctuation however does allow for this to take place + * after them, like the closing parenthesis for example. + * + * @param codePoint the code point after which we may want to apply the transformation + * @return whether it's fine to apply the transformation after this code point. + */ + private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) { + // TODO: This should probably be a blacklist rather than a whitelist. + // TODO: This should probably be language-dependant... + return Character.isLetterOrDigit(codePoint) + || codePoint == Constants.CODE_SINGLE_QUOTE + || codePoint == Constants.CODE_DOUBLE_QUOTE + || codePoint == Constants.CODE_CLOSING_PARENTHESIS + || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET + || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET + || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET + || codePoint == Constants.CODE_PLUS + || codePoint == Constants.CODE_PERCENT + || Character.getType(codePoint) == Character.OTHER_SYMBOL; + } + + /** + * Performs a recapitalization event. + * @param settingsValues The current settings values. + */ + private void performRecapitalization(final SettingsValues settingsValues) { + if (!mConnection.hasSelection()) { + return; // No selection + } + // If we have a recapitalize in progress, use it; otherwise, create a new one. + if (!mRecapitalizeStatus.isActive() + || !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd())) { + final CharSequence selectedText = + mConnection.getSelectedText(0 /* flags, 0 for no styles */); + if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection + mRecapitalizeStatus.initialize(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd(), selectedText.toString(), + settingsValues.mLocale, + settingsValues.mSpacingAndPunctuations.mSortedWordSeparators); + // We trim leading and trailing whitespace. + mRecapitalizeStatus.trim(); + } + mConnection.finishComposingText(); + mRecapitalizeStatus.rotate(); + final int numCharsDeleted = mConnection.getExpectedSelectionEnd() + - mConnection.getExpectedSelectionStart(); + mConnection.setSelection(mConnection.getExpectedSelectionEnd(), + mConnection.getExpectedSelectionEnd()); + mConnection.deleteSurroundingText(numCharsDeleted, 0); + mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); + mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(), + mRecapitalizeStatus.getNewCursorEnd()); + } + + private void performAdditionToUserHistoryDictionary(final SettingsValues settingsValues, + final String suggestion, final String prevWord) { + // 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. + if (!settingsValues.mCorrectionEnabled) return; + + if (TextUtils.isEmpty(suggestion)) return; + final boolean wasAutoCapitalized = + mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps(); + final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds( + System.currentTimeMillis()); + mSuggest.mDictionaryFacilitator.addToUserHistory(suggestion, wasAutoCapitalized, prevWord, + timeStampInSeconds); + } + + public void performUpdateSuggestionStripSync(final SettingsValues settingsValues) { + // Check if we have a suggestion engine attached. + if (!settingsValues.isSuggestionsRequested()) { + if (mWordComposer.isComposingWord()) { + Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " + + "requested!"); + } + return; + } + + if (!mWordComposer.isComposingWord() && !settingsValues.mBigramPredictionEnabled) { + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + return; + } + + final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>(); + mInputLogicHandler.getSuggestedWords(Suggest.SESSION_TYPING, + SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(final SuggestedWords suggestedWords) { + final String typedWord = mWordComposer.getTypedWord(); + // Show new suggestions if we have at least one. Otherwise keep the old + // suggestions with the new typed word. Exception: if the length of the + // typed word is <= 1 (after a deletion typically) we clear old suggestions. + if (suggestedWords.size() > 1 || typedWord.length() <= 1) { + holder.set(suggestedWords); + } else { + holder.set(retrieveOlderSuggestions(typedWord, mSuggestedWords)); + } + } + } + ); + + // This line may cause the current thread to wait. + final SuggestedWords suggestedWords = holder.get(null, + Constants.GET_SUGGESTED_WORDS_TIMEOUT); + if (suggestedWords != null) { + mSuggestionStripViewAccessor.showSuggestionStrip(suggestedWords); + } + } + + /** + * Check if the cursor is touching a word. If so, restart suggestions on this word, else + * do nothing. + * + * @param settingsValues the current values of the settings. + * @param includeResumedWordInSuggestions 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 includeResumedWordInSuggestions) { + // HACK: We may want to special-case some apps that exhibit bad behavior in case of + // recorrection. This is a temporary, stopgap measure that will be removed later. + // TODO: remove this. + if (settingsValues.isBrokenByRecorrection() + // Recorrection is not supported in languages without spaces because we don't know + // how to segment them yet. + || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces + // If no suggestions are requested, don't try restarting suggestions. + || !settingsValues.isSuggestionsRequested() + // If we are currently in a batch input, we must not resume suggestions, or the result + // of the batch input will replace the new composition. This may happen in the corner case + // that the app moves the cursor on its own accord during a batch input. + || mInputLogicHandler.isInBatchInput() + // If the cursor is not touching a word, or if there is a selection, return right away. + || mConnection.hasSelection() + // If we don't know the cursor location, return. + || mConnection.getExpectedSelectionStart() < 0) { + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + return; + } + final int expectedCursorPosition = mConnection.getExpectedSelectionStart(); + if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) { + // Show predictions. + mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( + WordComposer.CAPS_MODE_OFF, + getNthPreviousWordForSuggestion(settingsValues.mSpacingAndPunctuations, 1)); + mLatinIME.mHandler.postUpdateSuggestionStrip(); + return; + } + final TextRange range = mConnection.getWordRangeAtCursor( + settingsValues.mSpacingAndPunctuations.mSortedWordSeparators, + 0 /* additionalPrecedingWordsCount */); + if (null == range) return; // Happens if we don't have an input connection at all + if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out. + // If for some strange reason (editor bug or so) we measure the text before the cursor as + // longer than what the entire text is supposed to be, the safe thing to do is bail out. + if (range.mHasUrlSpans) return; // If there are links, we don't resume suggestions. Making + // edits to a linkified text through batch commands would ruin the URL spans, and unless + // we take very complicated steps to preserve the whole link, we can't do things right so + // we just do not resume because it's safer. + final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); + if (numberOfCharsInWordBeforeCursor > expectedCursorPosition) return; + final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); + final String typedWord = range.mWord.toString(); + if (includeResumedWordInSuggestions) { + 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; + } + int i = 0; + for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) { + for (final String s : span.getSuggestions()) { + ++i; + if (!TextUtils.equals(s, typedWord)) { + suggestions.add(new SuggestedWordInfo(s, + SuggestedWords.MAX_SUGGESTIONS - i, + SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, + SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, + SuggestedWordInfo.NOT_A_CONFIDENCE + /* autoCommitFirstWordConfidence */)); + } + } + } + final int[] codePoints = StringUtils.toCodePointArray(typedWord); + mWordComposer.setComposingWord(codePoints, + mLatinIME.getCoordinatesForCurrentKeyboard(codePoints), + getNthPreviousWordForSuggestion(settingsValues.mSpacingAndPunctuations, + // We want the previous word 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. + 0 == numberOfCharsInWordBeforeCursor ? 1 : 2)); + mWordComposer.setCursorPositionWithinWord( + typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor)); + mConnection.setComposingRegion(expectedCursorPosition - numberOfCharsInWordBeforeCursor, + expectedCursorPosition + range.getNumberOfCharsInWordAfterCursor()); + if (suggestions.isEmpty()) { + // We come here if there weren't any suggestion spans on this word. We will try to + // compute suggestions for it instead. + mInputLogicHandler.getSuggestedWords(Suggest.SESSION_TYPING, + SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords( + final SuggestedWords suggestedWordsIncludingTypedWord) { + final SuggestedWords suggestedWords; + if (suggestedWordsIncludingTypedWord.size() > 1 + && !includeResumedWordInSuggestions) { + // We were able to compute new suggestions for this word. + // Remove the typed word, since we don't want to display it in this + // case. The #getSuggestedWordsExcludingTypedWord() method sets + // willAutoCorrect to false. + suggestedWords = suggestedWordsIncludingTypedWord + .getSuggestedWordsExcludingTypedWord(); + } else { + // No saved suggestions, and we were unable to compute any good one + // either. Rather than displaying an empty suggestion strip, we'll + // display the original word alone in the middle. + // Since there is only one word, willAutoCorrect is false. + suggestedWords = suggestedWordsIncludingTypedWord; + } + mIsAutoCorrectionIndicatorOn = false; + mLatinIME.mHandler.showSuggestionStrip(suggestedWords); + }}); + } else { + // We found suggestion spans in the word. We'll create the SuggestedWords out of + // them, and make willAutoCorrect false. We make typedWordValid false, because the + // color of the word in the suggestion strip changes according to this parameter, + // and false gives the correct color. + final SuggestedWords suggestedWords = new SuggestedWords(suggestions, + null /* rawSuggestions */, typedWord, + false /* typedWordValid */, false /* willAutoCorrect */, + false /* isObsoleteSuggestions */, false /* isPrediction */, + SuggestedWords.NOT_A_SEQUENCE_NUMBER); + mIsAutoCorrectionIndicatorOn = false; + mLatinIME.mHandler.showSuggestionStrip(suggestedWords); + } + } + + /** + * Reverts a previous commit with auto-correction. + * + * This is triggered upon pressing backspace just after a commit with auto-correction. + * + * @param inputTransaction The transaction in progress. + */ + private void revertCommit(final InputTransaction inputTransaction) { + final String previousWord = mLastComposedWord.mPrevWord; + final CharSequence originallyTypedWord = mLastComposedWord.mTypedWord; + final CharSequence committedWord = mLastComposedWord.mCommittedWord; + final String committedWordString = committedWord.toString(); + final int cancelLength = committedWord.length(); + // We want java chars, not codepoints for the following. + final int separatorLength = mLastComposedWord.mSeparatorString.length(); + // TODO: should we check our saved separator against the actual contents of the text view? + final int deleteLength = cancelLength + separatorLength; + if (LatinImeLogger.sDBG) { + if (mWordComposer.isComposingWord()) { + throw new RuntimeException("revertCommit, but we are composing a word"); + } + final CharSequence wordBeforeCursor = + mConnection.getTextBeforeCursor(deleteLength, 0).subSequence(0, cancelLength); + if (!TextUtils.equals(committedWord, wordBeforeCursor)) { + throw new RuntimeException("revertCommit check failed: we thought we were " + + "reverting \"" + committedWord + + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); + } + } + mConnection.deleteSurroundingText(deleteLength, 0); + if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { + mSuggest.mDictionaryFacilitator.cancelAddingUserHistory( + previousWord, committedWordString); + } + final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; + final SpannableString textToCommit = new SpannableString(stringToCommit); + if (committedWord instanceof SpannableString) { + final SpannableString committedWordWithSuggestionSpans = (SpannableString)committedWord; + final Object[] spans = committedWordWithSuggestionSpans.getSpans(0, + committedWord.length(), Object.class); + final int lastCharIndex = textToCommit.length() - 1; + // We will collect all suggestions in the following array. + final ArrayList<String> suggestions = CollectionUtils.newArrayList(); + // First, add the committed word to the list of suggestions. + suggestions.add(committedWordString); + for (final Object span : spans) { + // If this is a suggestion span, we check that the locale is the right one, and + // that the word is not the committed word. That should mostly be the case. + // Given this, we add it to the list of suggestions, otherwise we discard it. + if (span instanceof SuggestionSpan) { + final SuggestionSpan suggestionSpan = (SuggestionSpan)span; + if (!suggestionSpan.getLocale().equals( + inputTransaction.mSettingsValues.mLocale.toString())) { + continue; + } + for (final String suggestion : suggestionSpan.getSuggestions()) { + if (!suggestion.equals(committedWordString)) { + suggestions.add(suggestion); + } + } + } else { + // If this is not a suggestion span, we just add it as is. + textToCommit.setSpan(span, 0 /* start */, lastCharIndex /* end */, + committedWordWithSuggestionSpans.getSpanFlags(span)); + } + } + // Add the suggestion list to the list of suggestions. + textToCommit.setSpan(new SuggestionSpan(inputTransaction.mSettingsValues.mLocale, + suggestions.toArray(new String[suggestions.size()]), 0 /* flags */), + 0 /* start */, lastCharIndex /* end */, 0 /* flags */); + } + if (inputTransaction.mSettingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) { + // For languages with spaces, we revert to the typed string, but the cursor is still + // after the separator so we don't resume suggestions. If the user wants to correct + // the word, they have to press backspace again. + mConnection.commitText(textToCommit, 1); + } 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. + final int[] codePoints = StringUtils.toCodePointArray(stringToCommit); + mWordComposer.setComposingWord(codePoints, + mLatinIME.getCoordinatesForCurrentKeyboard(codePoints), previousWord); + mConnection.setComposingText(textToCommit, 1); + } + if (inputTransaction.mSettingsValues.mIsInternal) { + LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_revertCommit(committedWord.toString(), + originallyTypedWord.toString(), + mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString); + } + // Don't restart suggestion yet. We'll restart if the user deletes the + // separator. + mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; + // We have a separator between the word and the cursor: we should show predictions. + inputTransaction.setRequiresUpdateSuggestions(); + } + + /** + * Factor in auto-caps and manual caps and compute the current caps mode. + * @param settingsValues the current settings values. + * @param keyboardShiftMode the current shift mode of the keyboard. See + * KeyboardSwitcher#getKeyboardShiftMode() for possible values. + * @return the actual caps mode the keyboard is in right now. + */ + private int getActualCapsMode(final SettingsValues settingsValues, + final int keyboardShiftMode) { + if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) { + return keyboardShiftMode; + } + final int auto = getCurrentAutoCapsState(settingsValues); + if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { + return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; + } + if (0 != auto) { + return WordComposer.CAPS_MODE_AUTO_SHIFTED; + } + return WordComposer.CAPS_MODE_OFF; + } + + /** + * Gets the current auto-caps state, factoring in the space state. + * + * This method tries its best to do this in the most efficient possible manner. It avoids + * getting text from the editor if possible at all. + * This is called from the KeyboardSwitcher (through a trampoline in LatinIME) because it + * needs to know auto caps state to display the right layout. + * + * @param settingsValues the relevant settings values + * @return a caps mode from TextUtils.CAP_MODE_* or Constants.TextUtils.CAP_MODE_OFF. + */ + public int getCurrentAutoCapsState(final SettingsValues settingsValues) { + if (!settingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; + + final EditorInfo ei = getCurrentInputEditorInfo(); + if (ei == null) return Constants.TextUtils.CAP_MODE_OFF; + final int inputType = ei.inputType; + // Warning: this depends on mSpaceState, which may not be the most current value. If + // mSpaceState gets updated later, whoever called this may need to be told about it. + return mConnection.getCursorCapsMode(inputType, settingsValues.mSpacingAndPunctuations, + SpaceState.PHANTOM == mSpaceState); + } + + public int getCurrentRecapitalizeState() { + if (!mRecapitalizeStatus.isActive() + || !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(), + mConnection.getExpectedSelectionEnd())) { + // Not recapitalizing at the moment + return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; + } + return mRecapitalizeStatus.getCurrentMode(); + } + + /** + * @return the editor info for the current editor + */ + private EditorInfo getCurrentInputEditorInfo() { + return mLatinIME.getCurrentInputEditorInfo(); + } + + /** + * Get 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 nth previous word before the cursor. + */ + // TODO: Make this private + public CharSequence getNthPreviousWordForSuggestion( + 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 from textview. + return mConnection.getNthPreviousWord(spacingAndPunctuations, nthPreviousWord); + } else { + return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null + : mLastComposedWord.mCommittedWord; + } + } + + /** + * Tests the passed word for resumability. + * + * We can resume suggestions on words whose first code point is a word code point (with some + * nuances: check the code for details). + * + * @param settings the current values of the settings. + * @param word the word to evaluate. + * @return whether it's fine to resume suggestions on this word. + */ + private static boolean isResumableWord(final SettingsValues settings, final String word) { + final int firstCodePoint = word.codePointAt(0); + return settings.isWordCodePoint(firstCodePoint) + && Constants.CODE_SINGLE_QUOTE != firstCodePoint + && Constants.CODE_DASH != firstCodePoint; + } + + /** + * @param actionId the action to perform + */ + private void performEditorAction(final int actionId) { + mConnection.performEditorAction(actionId); + } + + /** + * Perform the processing specific to inputting TLDs. + * + * Some keys input a TLD (specifically, the ".com" key) and this warrants some specific + * processing. First, if this is a TLD, we ignore PHANTOM spaces -- this is done by type + * of character in onCodeInput, but since this gets inputted as a whole string we need to + * do it here specifically. Then, if the last character before the cursor is a period, then + * we cut the dot at the start of ".com". This is because humans tend to type "www.google." + * and then press the ".com" key and instinctively don't expect to get "www.google..com". + * + * @param text the raw text supplied to onTextInput + * @return the text to actually send to the editor + */ + private String performSpecificTldProcessingOnTextInput(final String text) { + if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD + || !Character.isLetter(text.charAt(1))) { + // Not a tld: do nothing. + return text; + } + // We have a TLD (or something that looks like this): make sure we don't add + // a space even if currently in phantom mode. + mSpaceState = SpaceState.NONE; + final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); + // If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT. + if (Constants.CODE_PERIOD == codePointBeforeCursor) { + return text.substring(1); + } else { + return text; + } + } + + /** + * Handle a press on the settings key. + */ + private void onSettingsKeyPressed() { + mLatinIME.displaySettingsDialog(); + } + + /** + * Resets the whole input state to the starting state. + * + * This will clear the composing word, reset the last composed word, clear the suggestion + * strip and tell the input connection about it so that it can refresh its caches. + * + * @param newSelStart the new selection start, in java characters. + * @param newSelEnd the new selection end, in java characters. + * @param clearSuggestionStrip whether this method should clear the suggestion strip. + */ + // TODO: how is this different from startInput ?! + private void resetEntireInputState(final int newSelStart, final int newSelEnd, + final boolean clearSuggestionStrip) { + final boolean shouldFinishComposition = mWordComposer.isComposingWord(); + resetComposingState(true /* alsoResetLastComposedWord */); + if (clearSuggestionStrip) { + mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); + } + mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd, + shouldFinishComposition); + } + + /** + * Resets only the composing state. + * + * Compare #resetEntireInputState, which also clears the suggestion strip and resets the + * input connection caches. This only deals with the composing state. + * + * @param alsoResetLastComposedWord whether to also reset the last composed word. + */ + private void resetComposingState(final boolean alsoResetLastComposedWord) { + mWordComposer.reset(); + if (alsoResetLastComposedWord) { + mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; + } + } + + /** + * Make a {@link com.android.inputmethod.latin.SuggestedWords} object containing a typed word + * and obsolete suggestions. + * See {@link com.android.inputmethod.latin.SuggestedWords#getTypedWordAndPreviousSuggestions( + * String, com.android.inputmethod.latin.SuggestedWords)}. + * @param typedWord The typed word as a string. + * @param previousSuggestedWords The previously suggested words. + * @return Obsolete suggestions with the newly typed word. + */ + private SuggestedWords retrieveOlderSuggestions(final String typedWord, + final SuggestedWords previousSuggestedWords) { + final SuggestedWords oldSuggestedWords = + previousSuggestedWords.isPunctuationSuggestions() ? SuggestedWords.EMPTY + : previousSuggestedWords; + final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = + SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords); + return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */, + false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, + true /* isObsoleteSuggestions */, false /* isPrediction */); + } + + /** + * Gets a chunk of text with or the auto-correction indicator underline span as appropriate. + * + * This method looks at the old state of the auto-correction indicator to put or not put + * the underline span as appropriate. It is important to note that this does not correspond + * exactly to whether this word will be auto-corrected to or not: what's important here is + * to keep the same indication as before. + * When we add a new code point to a composing word, we don't know yet if we are going to + * auto-correct it until the suggestions are computed. But in the mean time, we still need + * to display the character and to extend the previous underline. To avoid any flickering, + * the underline should keep the same color it used to have, even if that's not ultimately + * the correct color for this new word. When the suggestions are finished evaluating, we + * will call this method again to fix the color of the underline. + * + * @param text the text on which to maybe apply the span. + * @return the same text, with the auto-correction underline span if that's appropriate. + */ + // TODO: Shouldn't this go in some *Utils class instead? + private CharSequence getTextWithUnderline(final String text) { + return mIsAutoCorrectionIndicatorOn + ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(mLatinIME, text) + : text; + } + + /** + * Sends a DOWN key event followed by an UP key event to the editor. + * + * If possible at all, avoid using this method. It causes all sorts of race conditions with + * the text view because it goes through a different, asynchronous binder. Also, batch edits + * are ignored for key events. Use the normal software input methods instead. + * + * @param keyCode the key code to send inside the key event. + */ + private void sendDownUpKeyEvent(final int keyCode) { + final long eventTime = SystemClock.uptimeMillis(); + mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, + KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); + mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, + KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); + } + + /** + * Sends a code point to the editor, using the most appropriate method. + * + * Normally we send code points with commitText, but there are some cases (where backward + * compatibility is a concern for example) where we want to use deprecated methods. + * + * @param settingsValues the current values of the settings. + * @param codePoint the code point to send. + */ + // TODO: replace these two parameters with an InputTransaction + private void sendKeyCodePoint(final SettingsValues settingsValues, final int codePoint) { + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_sendKeyCodePoint(codePoint); + } + // TODO: Remove this special handling of digit letters. + // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. + if (codePoint >= '0' && codePoint <= '9') { + sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0); + return; + } + + // TODO: we should do this also when the editor has TYPE_NULL + if (Constants.CODE_ENTER == codePoint && settingsValues.isBeforeJellyBean()) { + // 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. + sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER); + } else { + mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1); + } + } + + /** + * Promote a phantom space to an actual space. + * + * This essentially inserts a space, and that's it. It just checks the options and the text + * before the cursor are appropriate before doing it. + * + * @param settingsValues the current values of the settings. + */ + private void promotePhantomSpace(final SettingsValues settingsValues) { + if (settingsValues.shouldInsertSpacesAutomatically() + && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces + && !mConnection.textBeforeCursorLooksLikeURL()) { + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_promotePhantomSpace(); + } + sendKeyCodePoint(settingsValues, Constants.CODE_SPACE); + } + } + + /** + * Do the final processing after a batch input has ended. This commits the word to the editor. + * @param settingsValues the current values of the settings. + * @param suggestedWords suggestedWords to use. + */ + public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, + final SuggestedWords suggestedWords, + // TODO: remove this argument + final KeyboardSwitcher keyboardSwitcher) { + final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0); + if (TextUtils.isEmpty(batchInputText)) { + return; + } + mConnection.beginBatchEdit(); + if (SpaceState.PHANTOM == mSpaceState) { + promotePhantomSpace(settingsValues); + } + final SuggestedWordInfo autoCommitCandidate = mSuggestedWords.getAutoCommitCandidate(); + // Commit except the last word for phrase gesture if the top suggestion is eligible for auto + // commit. + if (settingsValues.mPhraseGestureEnabled && null != autoCommitCandidate) { + // Find the last space + final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; + if (0 != indexOfLastSpace) { + mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1); + final SuggestedWords suggestedWordsForLastWordOfPhraseGesture = + suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture(); + mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture); + } + final String lastWord = batchInputText.substring(indexOfLastSpace); + mWordComposer.setBatchInputWord(lastWord); + mConnection.setComposingText(lastWord, 1); + } else { + mWordComposer.setBatchInputWord(batchInputText); + mConnection.setComposingText(batchInputText, 1); + } + mConnection.endBatchEdit(); + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords); + } + // Space state must be updated before calling updateShiftState + mSpaceState = SpaceState.PHANTOM; + keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues), + getCurrentRecapitalizeState()); + } + + /** + * Commit the typed string to the editor. + * + * This is typically called when we should commit the currently composing word without applying + * auto-correction to it. Typically, we come here upon pressing a separator when the keyboard + * is configured to not do auto-correction at all (because of the settings or the properties of + * the editor). In this case, `separatorString' is set to the separator that was pressed. + * We also come here in a variety of cases with external user action. For example, when the + * cursor is moved while there is a composition, or when the keyboard is closed, or when the + * user presses the Send button for an SMS, we don't auto-correct as that would be unexpected. + * In this case, `separatorString' is set to NOT_A_SEPARATOR. + * + * @param settingsValues the current values of the settings. + * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none. + */ + // TODO: Make this private + public void commitTyped(final SettingsValues settingsValues, final String separatorString) { + if (!mWordComposer.isComposingWord()) return; + final String typedWord = mWordComposer.getTypedWord(); + if (typedWord.length() > 0) { + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode()); + } + commitChosenWord(settingsValues, typedWord, + LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString); + } + } + + /** + * Commit the current auto-correction. + * + * This will commit the best guess of the keyboard regarding what the user meant by typing + * the currently composing word. The IME computes suggestions and assigns a confidence score + * to each of them; when it's confident enough in one suggestion, it replaces the typed string + * by this suggestion at commit time. When it's not confident enough, or when it has no + * suggestions, or when the settings or environment does not allow for auto-correction, then + * this method just commits the typed string. + * Note that if suggestions are currently being computed in the background, this method will + * block until the computation returns. This is necessary for consistency (it would be very + * strange if pressing space would commit a different word depending on how fast you press). + * + * @param settingsValues the current value of the settings. + * @param separator the separator that's causing the commit to happen. + */ + private void commitCurrentAutoCorrection(final SettingsValues settingsValues, + final String separator, + // TODO: Remove this argument. + final LatinIME.UIHandler handler) { + // Complete any pending suggestions query first + if (handler.hasPendingUpdateSuggestions()) { + handler.cancelUpdateSuggestionStrip(); + performUpdateSuggestionStripSync(settingsValues); + } + final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); + final String typedWord = mWordComposer.getTypedWord(); + final String autoCorrection = (typedAutoCorrection != null) + ? typedAutoCorrection : typedWord; + if (autoCorrection != null) { + if (TextUtils.isEmpty(typedWord)) { + throw new RuntimeException("We have an auto-correction but the typed word " + + "is empty? Impossible! I must commit suicide."); + } + if (settingsValues.mIsInternal) { + LatinImeLoggerUtils.onAutoCorrection( + typedWord, autoCorrection, separator, mWordComposer); + } + if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { + final SuggestedWords suggestedWords = mSuggestedWords; + ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection, + separator, mWordComposer.isBatchMode(), suggestedWords); + } + commitChosenWord(settingsValues, autoCorrection, + LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator); + if (!typedWord.equals(autoCorrection)) { + // 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 + // the segment of text starting at the supplied index and running for the length + // 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)); + } + } + } + + /** + * Commits the chosen word to the text field and saves it for later retrieval. + * + * @param settingsValues the current values of the settings. + * @param chosenWord the word we want to commit. + * @param commitType the type of the commit, as one of LastComposedWord.COMMIT_TYPE_* + * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none. + */ + private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord, + final int commitType, final String separatorString) { + final SuggestedWords suggestedWords = mSuggestedWords; + final CharSequence chosenWordWithSuggestions = + SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, + suggestedWords); + mConnection.commitText(chosenWordWithSuggestions, 1); + // TODO: we pass 2 here, but would it be better to move this above and pass 1 instead? + final String prevWord = mConnection.getNthPreviousWord( + settingsValues.mSpacingAndPunctuations, 2); + // Add the word to the user history dictionary + performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWord); + // 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, prevWord); + final boolean shouldDiscardPreviousWordForSuggestion; + if (0 == StringUtils.codePointCount(separatorString)) { + // Separator is 0-length, we can keep the previous word for suggestion. Either this + // was a manual pick or the language has no spaces in which case we want to keep the + // previous word, or it was the keyboard closing or the cursor moving in which case it + // will be reset anyway. + shouldDiscardPreviousWordForSuggestion = false; + } else { + // Otherwise, we discard if the separator contains any non-whitespace. + shouldDiscardPreviousWordForSuggestion = + !StringUtils.containsOnlyWhitespace(separatorString); + } + if (shouldDiscardPreviousWordForSuggestion) { + mWordComposer.discardPreviousWordForSuggestion(); + } + } + + /** + * Retry resetting caches in the rich input connection. + * + * When the editor can't be accessed we can't reset the caches, so we schedule a retry. + * This method handles the retry, and re-schedules a new retry if we still can't access. + * We only retry up to 5 times before giving up. + * + * @param settingsValues the current values of the settings. + * @param tryResumeSuggestions Whether we should resume suggestions or not. + * @param remainingTries How many times we may try again before giving up. + * @return whether true if the caches were successfully reset, false otherwise. + */ + // TODO: make this private + public boolean retryResetCachesAndReturnSuccess(final SettingsValues settingsValues, + final boolean tryResumeSuggestions, final int remainingTries, + // TODO: remove these arguments + final LatinIME.UIHandler handler) { + if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess( + mConnection.getExpectedSelectionStart(), mConnection.getExpectedSelectionEnd(), + false /* shouldFinishComposition */)) { + if (0 < remainingTries) { + handler.postResetCaches(tryResumeSuggestions, remainingTries - 1); + return false; + } + // If remainingTries is 0, we should stop waiting for new tries, however we'll still + // return true as we need to perform other tasks (for example, loading the keyboard). + } + mConnection.tryFixLyingCursorPosition(); + if (tryResumeSuggestions) { + handler.postResumeSuggestions(); + } + return true; + } +} diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java new file mode 100644 index 000000000..9dbe2c38b --- /dev/null +++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogicHandler.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.inputlogic; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; + +import com.android.inputmethod.compat.LooperCompatUtils; +import com.android.inputmethod.latin.InputPointers; +import com.android.inputmethod.latin.LatinIME; +import com.android.inputmethod.latin.Suggest; +import com.android.inputmethod.latin.SuggestedWords; +import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; + +/** + * A helper to manage deferred tasks for the input logic. + */ +class InputLogicHandler implements Handler.Callback { + final Handler mNonUIThreadHandler; + // TODO: remove this reference. + final LatinIME mLatinIME; + final InputLogic mInputLogic; + private final Object mLock = new Object(); + private boolean mInBatchInput; // synchronized using {@link #mLock}. + + private static final int MSG_GET_SUGGESTED_WORDS = 1; + + // A handler that never does anything. This is used for cases where events come before anything + // is initialized, though probably only the monkey can actually do this. + public static final InputLogicHandler NULL_HANDLER = new InputLogicHandler() { + @Override + public void reset() {} + @Override + public boolean handleMessage(final Message msg) { return true; } + @Override + public void onStartBatchInput() {} + @Override + public void onUpdateBatchInput(final InputPointers batchPointers, + final int sequenceNumber) {} + @Override + public void onCancelBatchInput() {} + @Override + public void updateTailBatchInput(final InputPointers batchPointers, + final int sequenceNumber) {} + @Override + public void getSuggestedWords(final int sessionId, final int sequenceNumber, + final OnGetSuggestedWordsCallback callback) {} + }; + + private InputLogicHandler() { + mNonUIThreadHandler = null; + mLatinIME = null; + mInputLogic = null; + } + + public InputLogicHandler(final LatinIME latinIME, final InputLogic inputLogic) { + final HandlerThread handlerThread = new HandlerThread( + InputLogicHandler.class.getSimpleName()); + handlerThread.start(); + mNonUIThreadHandler = new Handler(handlerThread.getLooper(), this); + mLatinIME = latinIME; + mInputLogic = inputLogic; + } + + public void reset() { + mNonUIThreadHandler.removeCallbacksAndMessages(null); + } + + // In unit tests, we create several instances of LatinIME, which results in several instances + // of InputLogicHandler. To avoid these handlers lingering, we call this. + public void destroy() { + LooperCompatUtils.quitSafely(mNonUIThreadHandler.getLooper()); + } + + /** + * Handle a message. + * @see android.os.Handler.Callback#handleMessage(android.os.Message) + */ + // Called on the Non-UI handler thread by the Handler code. + @Override + public boolean handleMessage(final Message msg) { + switch (msg.what) { + case MSG_GET_SUGGESTED_WORDS: + mLatinIME.getSuggestedWords(msg.arg1 /* sessionId */, + msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj); + break; + } + return true; + } + + // Called on the UI thread by InputLogic. + public void onStartBatchInput() { + synchronized (mLock) { + mInBatchInput = true; + } + } + + public boolean isInBatchInput() { + return mInBatchInput; + } + + /** + * Fetch suggestions corresponding to an update of a batch input. + * @param batchPointers the updated pointers, including the part that was passed last time. + * @param sequenceNumber the sequence number associated with this batch input. + * @param isTailBatchInput true if this is the end of a batch input, false if it's an update. + */ + // This method can be called from any thread and will see to it that the correct threads + // are used for parts that require it. This method will send a message to the Non-UI handler + // thread to pull suggestions, and get the inlined callback to get called on the Non-UI + // handler thread. If this is the end of a batch input, the callback will then proceed to + // send a message to the UI handler in LatinIME so that showing suggestions can be done on + // the UI thread. + private void updateBatchInput(final InputPointers batchPointers, + final int sequenceNumber, final boolean isTailBatchInput) { + synchronized (mLock) { + if (!mInBatchInput) { + // Batch input has ended or canceled while the message was being delivered. + return; + } + mInputLogic.mWordComposer.setBatchInputPointers(batchPointers); + getSuggestedWords(Suggest.SESSION_GESTURE, sequenceNumber, + new OnGetSuggestedWordsCallback() { + @Override + public void onGetSuggestedWords(SuggestedWords suggestedWords) { + // We're now inside the callback. This always runs on the Non-UI thread, + // no matter what thread updateBatchInput was originally called on. + if (suggestedWords.isEmpty()) { + // Use old suggestions if we don't have any new ones. + // Previous suggestions are found in InputLogic#mSuggestedWords. + // Since these are the most recent ones and we just recomputed + // new ones to update them, then the previous ones are there. + suggestedWords = mInputLogic.mSuggestedWords; + } + mLatinIME.mHandler.showGesturePreviewAndSuggestionStrip(suggestedWords, + isTailBatchInput /* dismissGestureFloatingPreviewText */); + if (isTailBatchInput) { + mInBatchInput = false; + // The following call schedules onEndBatchInputInternal + // to be called on the UI thread. + mLatinIME.mHandler.showTailBatchInputResult(suggestedWords); + } + } + }); + } + } + + /** + * Update a batch input. + * + * This fetches suggestions and updates the suggestion strip and the floating text preview. + * + * @param batchPointers the updated batch pointers. + * @param sequenceNumber the sequence number associated with this batch input. + */ + // Called on the UI thread by InputLogic. + public void onUpdateBatchInput(final InputPointers batchPointers, + final int sequenceNumber) { + updateBatchInput(batchPointers, sequenceNumber, false /* isTailBatchInput */); + } + + /** + * Cancel a batch input. + * + * Note that as opposed to updateTailBatchInput, we do the UI side of this immediately on the + * same thread, rather than get this to call a method in LatinIME. This is because + * canceling a batch input does not necessitate the long operation of pulling suggestions. + */ + // Called on the UI thread by InputLogic. + public void onCancelBatchInput() { + synchronized (mLock) { + mInBatchInput = false; + } + } + + /** + * Trigger an update for a tail batch input. + * + * A tail batch input is the last update for a gesture, the one that is triggered after the + * user lifts their finger. This method schedules fetching suggestions on the non-UI thread, + * then when the suggestions are computed it comes back on the UI thread to update the + * suggestion strip, commit the first suggestion, and dismiss the floating text preview. + * + * @param batchPointers the updated batch pointers. + * @param sequenceNumber the sequence number associated with this batch input. + */ + // Called on the UI thread by InputLogic. + public void updateTailBatchInput(final InputPointers batchPointers, + final int sequenceNumber) { + updateBatchInput(batchPointers, sequenceNumber, true /* isTailBatchInput */); + } + + public void getSuggestedWords(final int sessionId, final int sequenceNumber, + final OnGetSuggestedWordsCallback callback) { + mNonUIThreadHandler.obtainMessage( + MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback).sendToTarget(); + } +} diff --git a/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java b/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java new file mode 100644 index 000000000..ce80c0016 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.inputlogic; + +/** + * Class for managing space states. + * + * At any given time, the input logic is in one of five possible space states. Depending on the + * current space state, some behavior will change; the prime example of this is the PHANTOM state, + * in which any subsequent letter input will input a space before the letter. Read on the + * description inside this class for each of the space states. + */ +public class SpaceState { + // None: the state where all the keyboard behavior is the most "standard" and no automatic + // input is added or removed. In this state, all self-inserting keys only insert themselves, + // and backspace removes one character. + public static final int NONE = 0; + // Double space: the state where the user pressed space twice quickly, which LatinIME + // resolved as period-space. In this state, pressing backspace will undo the + // double-space-to-period insertion: it will replace ". " with " ". + public static final int DOUBLE = 1; + // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip + // have just been swapped. In this state, pressing backspace will undo the swap: the + // characters will be swapped back back, and the space state will go to WEAK. + public static final int SWAP_PUNCTUATION = 2; + // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak + // spaces happen when the user presses space, accepting the current suggestion (whether + // it's an auto-correction or not). In this state, pressing a punctuation from the suggestion + // strip inserts it before the space (while it inserts it after the space in the NONE state). + public static final int WEAK = 3; + // Phantom space: a not-yet-inserted space that should get inserted on the next input, + // character provided it's not a separator. If it's a separator, the phantom space is dropped. + // Phantom spaces happen when a user chooses a word from the suggestion strip. In this state, + // non-separators insert a space before they get inserted. + public static final int PHANTOM = 4; + + private SpaceState() { + // This class is not publicly instantiable. + } +} diff --git a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java deleted file mode 100644 index fda97dafc..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/AbstractDictDecoder.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.TreeMap; - -/** - * A base class of the binary dictionary decoder. - */ -public abstract class AbstractDictDecoder implements DictDecoder { - protected FileHeader readHeader(final DictBuffer dictBuffer) - throws IOException, UnsupportedFormatException { - if (dictBuffer == null) { - openDictBuffer(); - } - - final int version = HeaderReader.readVersion(dictBuffer); - if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION - || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) { - throw new UnsupportedFormatException("Unsupported version : " + version); - } - // TODO: Remove this field. - final int optionsFlags = HeaderReader.readOptionFlags(dictBuffer); - - final int headerSize = HeaderReader.readHeaderSize(dictBuffer); - - if (headerSize < 0) { - throw new UnsupportedFormatException("header size can't be negative."); - } - - final HashMap<String, String> attributes = HeaderReader.readAttributes(dictBuffer, - headerSize); - - final FileHeader header = new FileHeader(headerSize, - new FusionDictionary.DictionaryOptions(attributes, - 0 != (optionsFlags & FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG), - 0 != (optionsFlags & FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG)), - new FormatOptions(version, - 0 != (optionsFlags & FormatSpec.SUPPORTS_DYNAMIC_UPDATE), - 0 != (optionsFlags & FormatSpec.CONTAINS_TIMESTAMP_FLAG))); - return header; - } - - @Override @UsedForTesting - public int getTerminalPosition(final String word) - throws IOException, UnsupportedFormatException { - if (!isDictBufferOpen()) { - openDictBuffer(); - } - return BinaryDictIOUtils.getTerminalPosition(this, word); - } - - @Override @UsedForTesting - public void readUnigramsAndBigramsBinary(final TreeMap<Integer, String> words, - final TreeMap<Integer, Integer> frequencies, - final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams) - throws IOException, UnsupportedFormatException { - if (!isDictBufferOpen()) { - openDictBuffer(); - } - BinaryDictIOUtils.readUnigramsAndBigramsBinary(this, words, frequencies, bigrams); - } - - /** - * A utility class for reading a file header. - */ - protected static class HeaderReader { - protected static int readVersion(final DictBuffer dictBuffer) - throws IOException, UnsupportedFormatException { - return BinaryDictDecoderUtils.checkFormatVersion(dictBuffer); - } - - protected static int readOptionFlags(final DictBuffer dictBuffer) { - return dictBuffer.readUnsignedShort(); - } - - protected static int readHeaderSize(final DictBuffer dictBuffer) { - return dictBuffer.readInt(); - } - - protected static HashMap<String, String> readAttributes(final DictBuffer dictBuffer, - final int headerSize) { - final HashMap<String, String> attributes = new HashMap<String, String>(); - while (dictBuffer.position() < headerSize) { - // We can avoid an infinite loop here since dictBuffer.position() is always - // increased by calling CharEncoding.readString. - final String key = CharEncoding.readString(dictBuffer); - final String value = CharEncoding.readString(dictBuffer); - attributes.put(key, value); - } - dictBuffer.position(headerSize); - return attributes; - } - } - - /** - * A utility class for reading a PtNode. - */ - protected static class PtNodeReader { - protected static int readPtNodeOptionFlags(final DictBuffer dictBuffer) { - return dictBuffer.readUnsignedByte(); - } - - protected static int readParentAddress(final DictBuffer dictBuffer, - final FormatOptions formatOptions) { - if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) { - return BinaryDictDecoderUtils.readSInt24(dictBuffer); - } else { - return FormatSpec.NO_PARENT_ADDRESS; - } - } - - protected static int readChildrenAddress(final DictBuffer dictBuffer, final int optionFlags, - final FormatOptions formatOptions) { - if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) { - final int address = BinaryDictDecoderUtils.readSInt24(dictBuffer); - if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS; - return address; - } else { - switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) { - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE: - return dictBuffer.readUnsignedByte(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES: - return dictBuffer.readUnsignedShort(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES: - return dictBuffer.readUnsignedInt24(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS: - default: - return FormatSpec.NO_CHILDREN_ADDRESS; - } - } - } - - // Reads shortcuts and returns the read length. - protected static int readShortcut(final DictBuffer dictBuffer, - final ArrayList<WeightedString> shortcutTargets) { - final int pointerBefore = dictBuffer.position(); - dictBuffer.readUnsignedShort(); // skip the size - while (true) { - final int targetFlags = dictBuffer.readUnsignedByte(); - final String word = CharEncoding.readString(dictBuffer); - shortcutTargets.add(new WeightedString(word, - targetFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY)); - if (0 == (targetFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break; - } - return dictBuffer.position() - pointerBefore; - } - - protected static int readBigramAddresses(final DictBuffer dictBuffer, - final ArrayList<PendingAttribute> bigrams, final int baseAddress) { - int readLength = 0; - int bigramCount = 0; - while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - final int bigramFlags = dictBuffer.readUnsignedByte(); - ++readLength; - final int sign = 0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE) - ? 1 : -1; - int bigramAddress = baseAddress + readLength; - switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) { - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE: - bigramAddress += sign * dictBuffer.readUnsignedByte(); - readLength += 1; - break; - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES: - bigramAddress += sign * dictBuffer.readUnsignedShort(); - readLength += 2; - break; - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES: - bigramAddress += sign * dictBuffer.readUnsignedInt24(); - readLength += 3; - break; - default: - throw new RuntimeException("Has bigrams with no address"); - } - bigrams.add(new PendingAttribute( - bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY, - bigramAddress)); - if (0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break; - } - return readLength; - } - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java deleted file mode 100644 index 216492b4d..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java +++ /dev/null @@ -1,623 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.Map; -import java.util.TreeMap; - -/** - * Decodes binary files for a FusionDictionary. - * - * All the methods in this class are static. - * - * TODO: Remove calls from classes except Ver3DictDecoder - * TODO: Move this file to makedict/internal. - * TODO: Rename this class to DictDecoderUtils. - */ -public final class BinaryDictDecoderUtils { - - private static final boolean DBG = MakedictLog.DBG; - - private BinaryDictDecoderUtils() { - // This utility class is not publicly instantiable. - } - - private static final int MAX_JUMPS = 12; - - @UsedForTesting - public interface DictBuffer { - public int readUnsignedByte(); - public int readUnsignedShort(); - public int readUnsignedInt24(); - public int readInt(); - public int position(); - public void position(int newPosition); - public void put(final byte b); - public int limit(); - @UsedForTesting - public int capacity(); - } - - public static final class ByteBufferDictBuffer implements DictBuffer { - private ByteBuffer mBuffer; - - public ByteBufferDictBuffer(final ByteBuffer buffer) { - mBuffer = buffer; - } - - @Override - public int readUnsignedByte() { - return mBuffer.get() & 0xFF; - } - - @Override - public int readUnsignedShort() { - return mBuffer.getShort() & 0xFFFF; - } - - @Override - public int readUnsignedInt24() { - final int retval = readUnsignedByte(); - return (retval << 16) + readUnsignedShort(); - } - - @Override - public int readInt() { - return mBuffer.getInt(); - } - - @Override - public int position() { - return mBuffer.position(); - } - - @Override - public void position(int newPos) { - mBuffer.position(newPos); - } - - @Override - public void put(final byte b) { - mBuffer.put(b); - } - - @Override - public int limit() { - return mBuffer.limit(); - } - - @Override - public int capacity() { - return mBuffer.capacity(); - } - } - - /** - * A class grouping utility function for our specific character encoding. - */ - static final class CharEncoding { - private static final int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20; - private static final int MAXIMAL_ONE_BYTE_CHARACTER_VALUE = 0xFF; - - /** - * Helper method to find out whether this code fits on one byte - */ - private static boolean fitsOnOneByte(final int character) { - return character >= MINIMAL_ONE_BYTE_CHARACTER_VALUE - && character <= MAXIMAL_ONE_BYTE_CHARACTER_VALUE; - } - - /** - * Compute the size of a character given its character code. - * - * Char format is: - * 1 byte = bbbbbbbb match - * case 000xxxxx: xxxxx << 16 + next byte << 8 + next byte - * else: if 00011111 (= 0x1F) : this is the terminator. This is a relevant choice because - * unicode code points range from 0 to 0x10FFFF, so any 3-byte value starting with - * 00011111 would be outside unicode. - * else: iso-latin-1 code - * This allows for the whole unicode range to be encoded, including chars outside of - * the BMP. Also everything in the iso-latin-1 charset is only 1 byte, except control - * characters which should never happen anyway (and still work, but take 3 bytes). - * - * @param character the character code. - * @return the size in binary encoded-form, either 1 or 3 bytes. - */ - static int getCharSize(final int character) { - // See char encoding in FusionDictionary.java - if (fitsOnOneByte(character)) return 1; - if (FormatSpec.INVALID_CHARACTER == character) return 1; - return 3; - } - - /** - * Compute the byte size of a character array. - */ - static int getCharArraySize(final int[] chars) { - int size = 0; - for (int character : chars) size += getCharSize(character); - return size; - } - - /** - * Writes a char array to a byte buffer. - * - * @param codePoints the code point array to write. - * @param buffer the byte buffer to write to. - * @param index the index in buffer to write the character array to. - * @return the index after the last character. - */ - static int writeCharArray(final int[] codePoints, final byte[] buffer, int index) { - for (int codePoint : codePoints) { - if (1 == getCharSize(codePoint)) { - buffer[index++] = (byte)codePoint; - } else { - buffer[index++] = (byte)(0xFF & (codePoint >> 16)); - buffer[index++] = (byte)(0xFF & (codePoint >> 8)); - buffer[index++] = (byte)(0xFF & codePoint); - } - } - return index; - } - - /** - * Writes a string with our character format to a byte buffer. - * - * This will also write the terminator byte. - * - * @param buffer the byte buffer to write to. - * @param origin the offset to write from. - * @param word the string to write. - * @return the size written, in bytes. - */ - static int writeString(final byte[] buffer, final int origin, - final String word) { - final int length = word.length(); - int index = origin; - for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = word.codePointAt(i); - if (1 == getCharSize(codePoint)) { - buffer[index++] = (byte)codePoint; - } else { - buffer[index++] = (byte)(0xFF & (codePoint >> 16)); - buffer[index++] = (byte)(0xFF & (codePoint >> 8)); - buffer[index++] = (byte)(0xFF & codePoint); - } - } - buffer[index++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR; - return index - origin; - } - - /** - * Writes a string with our character format to an OutputStream. - * - * This will also write the terminator byte. - * - * @param buffer the OutputStream to write to. - * @param word the string to write. - */ - static void writeString(final OutputStream buffer, final String word) throws IOException { - final int length = word.length(); - for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = word.codePointAt(i); - if (1 == getCharSize(codePoint)) { - buffer.write((byte) codePoint); - } else { - buffer.write((byte) (0xFF & (codePoint >> 16))); - buffer.write((byte) (0xFF & (codePoint >> 8))); - buffer.write((byte) (0xFF & codePoint)); - } - } - buffer.write(FormatSpec.PTNODE_CHARACTERS_TERMINATOR); - } - - /** - * Reads a string from a DictBuffer. This is the converse of the above method. - */ - static String readString(final DictBuffer dictBuffer) { - final StringBuilder s = new StringBuilder(); - int character = readChar(dictBuffer); - while (character != FormatSpec.INVALID_CHARACTER) { - s.appendCodePoint(character); - character = readChar(dictBuffer); - } - return s.toString(); - } - - /** - * Reads a character from the buffer. - * - * This follows the character format documented earlier in this source file. - * - * @param dictBuffer the buffer, positioned over an encoded character. - * @return the character code. - */ - static int readChar(final DictBuffer dictBuffer) { - int character = dictBuffer.readUnsignedByte(); - if (!fitsOnOneByte(character)) { - if (FormatSpec.PTNODE_CHARACTERS_TERMINATOR == character) { - return FormatSpec.INVALID_CHARACTER; - } - character <<= 16; - character += dictBuffer.readUnsignedShort(); - } - return character; - } - } - - // Input methods: Read a binary dictionary to memory. - // readDictionaryBinary is the public entry point for them. - - static int readSInt24(final DictBuffer dictBuffer) { - final int retval = dictBuffer.readUnsignedInt24(); - final int sign = ((retval & FormatSpec.MSB24) != 0) ? -1 : 1; - return sign * (retval & FormatSpec.SINT24_MAX); - } - - static int readChildrenAddress(final DictBuffer dictBuffer, - final int optionFlags, final FormatOptions options) { - if (options.mSupportsDynamicUpdate) { - final int address = dictBuffer.readUnsignedInt24(); - if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS; - if ((address & FormatSpec.MSB24) != 0) { - return -(address & FormatSpec.SINT24_MAX); - } else { - return address; - } - } - switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) { - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE: - return dictBuffer.readUnsignedByte(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES: - return dictBuffer.readUnsignedShort(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES: - return dictBuffer.readUnsignedInt24(); - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS: - default: - return FormatSpec.NO_CHILDREN_ADDRESS; - } - } - - static int readParentAddress(final DictBuffer dictBuffer, - final FormatOptions formatOptions) { - if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) { - final int parentAddress = dictBuffer.readUnsignedInt24(); - final int sign = ((parentAddress & FormatSpec.MSB24) != 0) ? -1 : 1; - return sign * (parentAddress & FormatSpec.SINT24_MAX); - } else { - return FormatSpec.NO_PARENT_ADDRESS; - } - } - - /** - * Reads and returns the PtNode count out of a buffer and forwards the pointer. - */ - /* package */ static int readPtNodeCount(final DictBuffer dictBuffer) { - final int msb = dictBuffer.readUnsignedByte(); - if (FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT >= msb) { - return msb; - } else { - return ((FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT & msb) << 8) - + dictBuffer.readUnsignedByte(); - } - } - - /** - * Finds, as a string, the word at the position passed as an argument. - * - * @param dictDecoder the dict decoder. - * @param headerSize the size of the header. - * @param pos the position to seek. - * @param formatOptions file format options. - * @return the word with its frequency, as a weighted string. - */ - /* package for tests */ static WeightedString getWordAtPosition(final DictDecoder dictDecoder, - final int headerSize, final int pos, final FormatOptions formatOptions) { - final WeightedString result; - final int originalPos = dictDecoder.getPosition(); - dictDecoder.setPosition(pos); - - if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) { - result = getWordAtPositionWithParentAddress(dictDecoder, pos, formatOptions); - } else { - result = getWordAtPositionWithoutParentAddress(dictDecoder, headerSize, pos, - formatOptions); - } - - dictDecoder.setPosition(originalPos); - return result; - } - - @SuppressWarnings("unused") - private static WeightedString getWordAtPositionWithParentAddress(final DictDecoder dictDecoder, - final int pos, final FormatOptions options) { - int currentPos = pos; - int frequency = Integer.MIN_VALUE; - final StringBuilder builder = new StringBuilder(); - // the length of the path from the root to the leaf is limited by MAX_WORD_LENGTH - for (int count = 0; count < FormatSpec.MAX_WORD_LENGTH; ++count) { - PtNodeInfo currentInfo; - int loopCounter = 0; - do { - dictDecoder.setPosition(currentPos); - currentInfo = dictDecoder.readPtNode(currentPos, options); - if (BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags, options)) { - currentPos = currentInfo.mParentAddress + currentInfo.mOriginalAddress; - } - if (DBG && loopCounter++ > MAX_JUMPS) { - MakedictLog.d("Too many jumps - probably a bug"); - } - } while (BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags, options)); - if (Integer.MIN_VALUE == frequency) frequency = currentInfo.mFrequency; - builder.insert(0, - new String(currentInfo.mCharacters, 0, currentInfo.mCharacters.length)); - if (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) break; - currentPos = currentInfo.mParentAddress + currentInfo.mOriginalAddress; - } - return new WeightedString(builder.toString(), frequency); - } - - private static WeightedString getWordAtPositionWithoutParentAddress( - final DictDecoder dictDecoder, final int headerSize, final int pos, - final FormatOptions options) { - dictDecoder.setPosition(headerSize); - final int count = dictDecoder.readPtNodeCount(); - int groupPos = headerSize + BinaryDictIOUtils.getPtNodeCountSize(count); - final StringBuilder builder = new StringBuilder(); - WeightedString result = null; - - PtNodeInfo last = null; - for (int i = count - 1; i >= 0; --i) { - PtNodeInfo info = dictDecoder.readPtNode(groupPos, options); - groupPos = info.mEndAddress; - if (info.mOriginalAddress == pos) { - builder.append(new String(info.mCharacters, 0, info.mCharacters.length)); - result = new WeightedString(builder.toString(), info.mFrequency); - break; // and return - } - if (BinaryDictIOUtils.hasChildrenAddress(info.mChildrenAddress)) { - if (info.mChildrenAddress > pos) { - if (null == last) continue; - builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); - dictDecoder.setPosition(last.mChildrenAddress); - i = dictDecoder.readPtNodeCount(); - groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i); - last = null; - continue; - } - last = info; - } - if (0 == i && BinaryDictIOUtils.hasChildrenAddress(last.mChildrenAddress)) { - builder.append(new String(last.mCharacters, 0, last.mCharacters.length)); - dictDecoder.setPosition(last.mChildrenAddress); - i = dictDecoder.readPtNodeCount(); - groupPos = last.mChildrenAddress + BinaryDictIOUtils.getPtNodeCountSize(i); - last = null; - continue; - } - } - return result; - } - - /** - * Reads a single node array from a buffer. - * - * This methods reads the file at the current position. A node array is fully expected to start - * at the current position. - * This will recursively read other node arrays into the structure, populating the reverse - * maps on the fly and using them to keep track of already read nodes. - * - * @param dictDecoder the dict decoder, correctly positioned at the start of a node array. - * @param headerSize the size, in bytes, of the file header. - * @param reverseNodeArrayMap a mapping from addresses to already read node arrays. - * @param reversePtNodeMap a mapping from addresses to already read PtNodes. - * @param options file format options. - * @return the read node array with all his children already read. - */ - private static PtNodeArray readNodeArray(final DictDecoder dictDecoder, - final int headerSize, final Map<Integer, PtNodeArray> reverseNodeArrayMap, - final Map<Integer, PtNode> reversePtNodeMap, final FormatOptions options) - throws IOException { - final ArrayList<PtNode> nodeArrayContents = new ArrayList<PtNode>(); - final int nodeArrayOriginPos = dictDecoder.getPosition(); - - do { // Scan the linked-list node. - final int nodeArrayHeadPos = dictDecoder.getPosition(); - final int count = dictDecoder.readPtNodeCount(); - int groupOffsetPos = nodeArrayHeadPos + BinaryDictIOUtils.getPtNodeCountSize(count); - for (int i = count; i > 0; --i) { // Scan the array of PtNode. - PtNodeInfo info = dictDecoder.readPtNode(groupOffsetPos, options); - if (BinaryDictIOUtils.isMovedPtNode(info.mFlags, options)) continue; - ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets; - ArrayList<WeightedString> bigrams = null; - if (null != info.mBigrams) { - bigrams = new ArrayList<WeightedString>(); - for (PendingAttribute bigram : info.mBigrams) { - final WeightedString word = getWordAtPosition(dictDecoder, headerSize, - bigram.mAddress, options); - final int reconstructedFrequency = - BinaryDictIOUtils.reconstructBigramFrequency(word.mFrequency, - bigram.mFrequency); - bigrams.add(new WeightedString(word.mWord, reconstructedFrequency)); - } - } - if (BinaryDictIOUtils.hasChildrenAddress(info.mChildrenAddress)) { - PtNodeArray children = reverseNodeArrayMap.get(info.mChildrenAddress); - if (null == children) { - final int currentPosition = dictDecoder.getPosition(); - dictDecoder.setPosition(info.mChildrenAddress); - children = readNodeArray(dictDecoder, headerSize, reverseNodeArrayMap, - reversePtNodeMap, options); - dictDecoder.setPosition(currentPosition); - } - nodeArrayContents.add( - new PtNode(info.mCharacters, shortcutTargets, bigrams, - info.mFrequency, - 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD), - 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED), children)); - } else { - nodeArrayContents.add( - new PtNode(info.mCharacters, shortcutTargets, bigrams, - info.mFrequency, - 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD), - 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED))); - } - groupOffsetPos = info.mEndAddress; - } - - // reach the end of the array. - if (options.mSupportsDynamicUpdate) { - final boolean hasValidForwardLink = dictDecoder.readAndFollowForwardLink(); - if (!hasValidForwardLink) break; - } - } while (options.mSupportsDynamicUpdate && dictDecoder.hasNextPtNodeArray()); - - final PtNodeArray nodeArray = new PtNodeArray(nodeArrayContents); - nodeArray.mCachedAddressBeforeUpdate = nodeArrayOriginPos; - nodeArray.mCachedAddressAfterUpdate = nodeArrayOriginPos; - reverseNodeArrayMap.put(nodeArray.mCachedAddressAfterUpdate, nodeArray); - return nodeArray; - } - - /** - * Helper function to get the binary format version from the header. - * @throws IOException - */ - private static int getFormatVersion(final DictBuffer dictBuffer) - throws IOException { - final int magic = dictBuffer.readInt(); - if (FormatSpec.MAGIC_NUMBER == magic) return dictBuffer.readUnsignedShort(); - return FormatSpec.NOT_A_VERSION_NUMBER; - } - - /** - * Helper function to get and validate the binary format version. - * @throws UnsupportedFormatException - * @throws IOException - */ - static int checkFormatVersion(final DictBuffer dictBuffer) - throws IOException, UnsupportedFormatException { - final int version = getFormatVersion(dictBuffer); - if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION - || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) { - throw new UnsupportedFormatException("This file has version " + version - + ", but this implementation does not support versions above " - + FormatSpec.MAXIMUM_SUPPORTED_VERSION); - } - return version; - } - - /** - * Reads a buffer and returns the memory representation of the dictionary. - * - * This high-level method takes a buffer and reads its contents, populating a - * FusionDictionary structure. The optional dict argument is an existing dictionary to - * which words from the buffer should be added. If it is null, a new dictionary is created. - * - * @param dictDecoder the dict decoder. - * @param dict an optional dictionary to add words to, or null. - * @return the created (or merged) dictionary. - */ - @UsedForTesting - /* package */ static FusionDictionary readDictionaryBinary(final DictDecoder dictDecoder, - final FusionDictionary dict) throws IOException, UnsupportedFormatException { - // Read header - final FileHeader fileHeader = dictDecoder.readHeader(); - - Map<Integer, PtNodeArray> reverseNodeArrayMapping = new TreeMap<Integer, PtNodeArray>(); - Map<Integer, PtNode> reversePtNodeMapping = new TreeMap<Integer, PtNode>(); - final PtNodeArray root = readNodeArray(dictDecoder, fileHeader.mHeaderSize, - reverseNodeArrayMapping, reversePtNodeMapping, fileHeader.mFormatOptions); - - FusionDictionary newDict = new FusionDictionary(root, fileHeader.mDictionaryOptions); - if (null != dict) { - for (final Word w : dict) { - if (w.mIsBlacklistEntry) { - newDict.addBlacklistEntry(w.mWord, w.mShortcutTargets, w.mIsNotAWord); - } else { - newDict.add(w.mWord, w.mFrequency, w.mShortcutTargets, w.mIsNotAWord); - } - } - for (final Word w : dict) { - // By construction a binary dictionary may not have bigrams pointing to - // words that are not also registered as unigrams so we don't have to avoid - // them explicitly here. - for (final WeightedString bigram : w.mBigrams) { - newDict.setBigram(w.mWord, bigram.mWord, bigram.mFrequency); - } - } - } - - return newDict; - } - - /** - * Helper method to pass a file name instead of a File object to isBinaryDictionary. - */ - public static boolean isBinaryDictionary(final String filename) { - final File file = new File(filename); - return isBinaryDictionary(file); - } - - /** - * Basic test to find out whether the file is a binary dictionary or not. - * - * Concretely this only tests the magic number. - * - * @param file The file to test. - * @return true if it's a binary dictionary, false otherwise - */ - public static boolean isBinaryDictionary(final File file) { - FileInputStream inStream = null; - try { - inStream = new FileInputStream(file); - final ByteBuffer buffer = inStream.getChannel().map( - FileChannel.MapMode.READ_ONLY, 0, file.length()); - final int version = getFormatVersion(new ByteBufferDictBuffer(buffer)); - return (version >= FormatSpec.MINIMUM_SUPPORTED_VERSION - && version <= FormatSpec.MAXIMUM_SUPPORTED_VERSION); - } catch (FileNotFoundException e) { - return false; - } catch (IOException e) { - return false; - } finally { - if (inStream != null) { - try { - inStream.close(); - } catch (IOException e) { - // do nothing - } - } - } - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java deleted file mode 100644 index f761829de..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java +++ /dev/null @@ -1,956 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; - -/** - * Encodes binary files for a FusionDictionary. - * - * All the methods in this class are static. - * - * TODO: Rename this class to DictEncoderUtils. - */ -public class BinaryDictEncoderUtils { - - private static final boolean DBG = MakedictLog.DBG; - - private BinaryDictEncoderUtils() { - // This utility class is not publicly instantiable. - } - - // Arbitrary limit to how much passes we consider address size compression should - // terminate in. At the time of this writing, our largest dictionary completes - // compression in five passes. - // If the number of passes exceeds this number, makedict bails with an exception on - // suspicion that a bug might be causing an infinite loop. - private static final int MAX_PASSES = 24; - - /** - * Compute the binary size of the character array. - * - * If only one character, this is the size of this character. If many, it's the sum of their - * sizes + 1 byte for the terminator. - * - * @param characters the character array - * @return the size of the char array, including the terminator if any - */ - static int getPtNodeCharactersSize(final int[] characters) { - int size = CharEncoding.getCharArraySize(characters); - if (characters.length > 1) size += FormatSpec.PTNODE_TERMINATOR_SIZE; - return size; - } - - /** - * Compute the binary size of the character array in a PtNode - * - * If only one character, this is the size of this character. If many, it's the sum of their - * sizes + 1 byte for the terminator. - * - * @param ptNode the PtNode - * @return the size of the char array, including the terminator if any - */ - private static int getPtNodeCharactersSize(final PtNode ptNode) { - return getPtNodeCharactersSize(ptNode.mChars); - } - - /** - * Compute the binary size of the PtNode count for a node array. - * @param nodeArray the nodeArray - * @return the size of the PtNode count, either 1 or 2 bytes. - */ - private static int getPtNodeCountSize(final PtNodeArray nodeArray) { - return BinaryDictIOUtils.getPtNodeCountSize(nodeArray.mData.size()); - } - - /** - * Compute the size of a shortcut in bytes. - */ - private static int getShortcutSize(final WeightedString shortcut) { - int size = FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE; - final String word = shortcut.mWord; - final int length = word.length(); - for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = word.codePointAt(i); - size += CharEncoding.getCharSize(codePoint); - } - size += FormatSpec.PTNODE_TERMINATOR_SIZE; - return size; - } - - /** - * Compute the size of a shortcut list in bytes. - * - * This is known in advance and does not change according to position in the file - * like address lists do. - */ - static int getShortcutListSize(final ArrayList<WeightedString> shortcutList) { - if (null == shortcutList || shortcutList.isEmpty()) return 0; - int size = FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE; - for (final WeightedString shortcut : shortcutList) { - size += getShortcutSize(shortcut); - } - return size; - } - - /** - * Compute the maximum size of a PtNode, assuming 3-byte addresses for everything. - * - * @param ptNode the PtNode to compute the size of. - * @param options file format options. - * @return the maximum size of the PtNode. - */ - private static int getPtNodeMaximumSize(final PtNode ptNode, final FormatOptions options) { - int size = getNodeHeaderSize(ptNode, options); - if (ptNode.isTerminal()) { - // If terminal, one byte for the frequency or four bytes for the terminal id. - if (options.mHasTerminalId) { - size += FormatSpec.PTNODE_TERMINAL_ID_SIZE; - } else { - size += FormatSpec.PTNODE_FREQUENCY_SIZE; - } - } - size += FormatSpec.PTNODE_MAX_ADDRESS_SIZE; // For children address - size += getShortcutListSize(ptNode.mShortcutTargets); - if (null != ptNode.mBigrams) { - size += (FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE - + FormatSpec.PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE) - * ptNode.mBigrams.size(); - } - return size; - } - - /** - * Compute the maximum size of each PtNode of a PtNode array, assuming 3-byte addresses for - * everything, and caches it in the `mCachedSize' member of the nodes; deduce the size of - * the containing node array, and cache it it its 'mCachedSize' member. - * - * @param ptNodeArray the node array to compute the maximum size of. - * @param options file format options. - */ - private static void calculatePtNodeArrayMaximumSize(final PtNodeArray ptNodeArray, - final FormatOptions options) { - int size = getPtNodeCountSize(ptNodeArray); - for (PtNode node : ptNodeArray.mData) { - final int nodeSize = getPtNodeMaximumSize(node, options); - node.mCachedSize = nodeSize; - size += nodeSize; - } - if (options.mSupportsDynamicUpdate) { - size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE; - } - ptNodeArray.mCachedSize = size; - } - - /** - * Compute the size of the header (flag + [parent address] + characters size) of a PtNode. - * - * @param ptNode the PtNode of which to compute the size of the header - * @param options file format options. - */ - private static int getNodeHeaderSize(final PtNode ptNode, final FormatOptions options) { - if (BinaryDictIOUtils.supportsDynamicUpdate(options)) { - return FormatSpec.PTNODE_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE - + getPtNodeCharactersSize(ptNode); - } else { - return FormatSpec.PTNODE_FLAGS_SIZE + getPtNodeCharactersSize(ptNode); - } - } - - /** - * Compute the size, in bytes, that an address will occupy. - * - * This can be used either for children addresses (which are always positive) or for - * attribute, which may be positive or negative but - * store their sign bit separately. - * - * @param address the address - * @return the byte size. - */ - static int getByteSize(final int address) { - assert(address <= FormatSpec.UINT24_MAX); - if (!BinaryDictIOUtils.hasChildrenAddress(address)) { - return 0; - } else if (Math.abs(address) <= FormatSpec.UINT8_MAX) { - return 1; - } else if (Math.abs(address) <= FormatSpec.UINT16_MAX) { - return 2; - } else { - return 3; - } - } - - static int writeUIntToBuffer(final byte[] buffer, int position, final int value, - final int size) { - switch(size) { - case 4: - buffer[position++] = (byte) ((value >> 24) & 0xFF); - /* fall through */ - case 3: - buffer[position++] = (byte) ((value >> 16) & 0xFF); - /* fall through */ - case 2: - buffer[position++] = (byte) ((value >> 8) & 0xFF); - /* fall through */ - case 1: - buffer[position++] = (byte) (value & 0xFF); - break; - default: - /* nop */ - } - return position; - } - - static void writeUIntToStream(final OutputStream stream, final int value, final int size) - throws IOException { - switch(size) { - case 4: - stream.write((value >> 24) & 0xFF); - /* fall through */ - case 3: - stream.write((value >> 16) & 0xFF); - /* fall through */ - case 2: - stream.write((value >> 8) & 0xFF); - /* fall through */ - case 1: - stream.write(value & 0xFF); - break; - default: - /* nop */ - } - } - - // End utility methods - - // This method is responsible for finding a nice ordering of the nodes that favors run-time - // cache performance and dictionary size. - /* package for tests */ static ArrayList<PtNodeArray> flattenTree( - final PtNodeArray rootNodeArray) { - final int treeSize = FusionDictionary.countPtNodes(rootNodeArray); - MakedictLog.i("Counted nodes : " + treeSize); - final ArrayList<PtNodeArray> flatTree = new ArrayList<PtNodeArray>(treeSize); - return flattenTreeInner(flatTree, rootNodeArray); - } - - private static ArrayList<PtNodeArray> flattenTreeInner(final ArrayList<PtNodeArray> list, - final PtNodeArray ptNodeArray) { - // Removing the node is necessary if the tails are merged, because we would then - // add the same node several times when we only want it once. A number of places in - // the code also depends on any node being only once in the list. - // Merging tails can only be done if there are no attributes. Searching for attributes - // in LatinIME code depends on a total breadth-first ordering, which merging tails - // breaks. If there are no attributes, it should be fine (and reduce the file size) - // to merge tails, and removing the node from the list would be necessary. However, - // we don't merge tails because breaking the breadth-first ordering would result in - // extreme overhead at bigram lookup time (it would make the search function O(n) instead - // of the current O(log(n)), where n=number of nodes in the dictionary which is pretty - // high). - // If no nodes are ever merged, we can't have the same node twice in the list, hence - // searching for duplicates in unnecessary. It is also very performance consuming, - // since `list' is an ArrayList so it's an O(n) operation that runs on all nodes, making - // this simple list.remove operation O(n*n) overall. On Android this overhead is very - // high. - // For future reference, the code to remove duplicate is a simple : list.remove(node); - list.add(ptNodeArray); - final ArrayList<PtNode> branches = ptNodeArray.mData; - for (PtNode ptNode : branches) { - if (null != ptNode.mChildren) flattenTreeInner(list, ptNode.mChildren); - } - return list; - } - - /** - * Get the offset from a position inside a current node array to a target node array, during - * update. - * - * If the current node array is before the target node array, the target node array has not - * been updated yet, so we should return the offset from the old position of the current node - * array to the old position of the target node array. If on the other hand the target is - * before the current node array, it already has been updated, so we should return the offset - * from the new position in the current node array to the new position in the target node - * array. - * - * @param currentNodeArray node array containing the PtNode where the offset will be written - * @param offsetFromStartOfCurrentNodeArray offset, in bytes, from the start of currentNodeArray - * @param targetNodeArray the target node array to get the offset to - * @return the offset to the target node array - */ - private static int getOffsetToTargetNodeArrayDuringUpdate(final PtNodeArray currentNodeArray, - final int offsetFromStartOfCurrentNodeArray, final PtNodeArray targetNodeArray) { - final boolean isTargetBeforeCurrent = (targetNodeArray.mCachedAddressBeforeUpdate - < currentNodeArray.mCachedAddressBeforeUpdate); - if (isTargetBeforeCurrent) { - return targetNodeArray.mCachedAddressAfterUpdate - - (currentNodeArray.mCachedAddressAfterUpdate - + offsetFromStartOfCurrentNodeArray); - } else { - return targetNodeArray.mCachedAddressBeforeUpdate - - (currentNodeArray.mCachedAddressBeforeUpdate - + offsetFromStartOfCurrentNodeArray); - } - } - - /** - * Get the offset from a position inside a current node array to a target PtNode, during - * update. - * - * @param currentNodeArray node array containing the PtNode where the offset will be written - * @param offsetFromStartOfCurrentNodeArray offset, in bytes, from the start of currentNodeArray - * @param targetPtNode the target PtNode to get the offset to - * @return the offset to the target PtNode - */ - // TODO: is there any way to factorize this method with the one above? - private static int getOffsetToTargetPtNodeDuringUpdate(final PtNodeArray currentNodeArray, - final int offsetFromStartOfCurrentNodeArray, final PtNode targetPtNode) { - final int oldOffsetBasePoint = currentNodeArray.mCachedAddressBeforeUpdate - + offsetFromStartOfCurrentNodeArray; - final boolean isTargetBeforeCurrent = (targetPtNode.mCachedAddressBeforeUpdate - < oldOffsetBasePoint); - // If the target is before the current node array, then its address has already been - // updated. We can use the AfterUpdate member, and compare it to our own member after - // update. Otherwise, the AfterUpdate member is not updated yet, so we need to use the - // BeforeUpdate member, and of course we have to compare this to our own address before - // update. - if (isTargetBeforeCurrent) { - final int newOffsetBasePoint = currentNodeArray.mCachedAddressAfterUpdate - + offsetFromStartOfCurrentNodeArray; - return targetPtNode.mCachedAddressAfterUpdate - newOffsetBasePoint; - } else { - return targetPtNode.mCachedAddressBeforeUpdate - oldOffsetBasePoint; - } - } - - /** - * Computes the actual node array size, based on the cached addresses of the children nodes. - * - * Each node array stores its tentative address. During dictionary address computing, these - * are not final, but they can be used to compute the node array size (the node array size - * depends on the address of the children because the number of bytes necessary to store an - * address depends on its numeric value. The return value indicates whether the node array - * contents (as in, any of the addresses stored in the cache fields) have changed with - * respect to their previous value. - * - * @param ptNodeArray the node array to compute the size of. - * @param dict the dictionary in which the word/attributes are to be found. - * @param formatOptions file format options. - * @return false if none of the cached addresses inside the node array changed, true otherwise. - */ - private static boolean computeActualPtNodeArraySize(final PtNodeArray ptNodeArray, - final FusionDictionary dict, final FormatOptions formatOptions) { - boolean changed = false; - int size = getPtNodeCountSize(ptNodeArray); - for (PtNode ptNode : ptNodeArray.mData) { - ptNode.mCachedAddressAfterUpdate = ptNodeArray.mCachedAddressAfterUpdate + size; - if (ptNode.mCachedAddressAfterUpdate != ptNode.mCachedAddressBeforeUpdate) { - changed = true; - } - int nodeSize = getNodeHeaderSize(ptNode, formatOptions); - if (ptNode.isTerminal()) { - if (formatOptions.mHasTerminalId) { - nodeSize += FormatSpec.PTNODE_TERMINAL_ID_SIZE; - } else { - nodeSize += FormatSpec.PTNODE_FREQUENCY_SIZE; - } - } - if (formatOptions.mSupportsDynamicUpdate) { - nodeSize += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE; - } else if (null != ptNode.mChildren) { - nodeSize += getByteSize(getOffsetToTargetNodeArrayDuringUpdate(ptNodeArray, - nodeSize + size, ptNode.mChildren)); - } - if (formatOptions.mVersion < FormatSpec.FIRST_VERSION_WITH_TERMINAL_ID) { - nodeSize += getShortcutListSize(ptNode.mShortcutTargets); - if (null != ptNode.mBigrams) { - for (WeightedString bigram : ptNode.mBigrams) { - final int offset = getOffsetToTargetPtNodeDuringUpdate(ptNodeArray, - nodeSize + size + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE, - FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord)); - nodeSize += getByteSize(offset) + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE; - } - } - } - ptNode.mCachedSize = nodeSize; - size += nodeSize; - } - if (formatOptions.mSupportsDynamicUpdate) { - size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE; - } - if (ptNodeArray.mCachedSize != size) { - ptNodeArray.mCachedSize = size; - changed = true; - } - return changed; - } - - /** - * Initializes the cached addresses of node arrays and their containing nodes from their size. - * - * @param flatNodes the list of node arrays. - * @param formatOptions file format options. - * @return the byte size of the entire stack. - */ - private static int initializePtNodeArraysCachedAddresses(final ArrayList<PtNodeArray> flatNodes, - final FormatOptions formatOptions) { - int nodeArrayOffset = 0; - for (final PtNodeArray nodeArray : flatNodes) { - nodeArray.mCachedAddressBeforeUpdate = nodeArrayOffset; - int nodeCountSize = getPtNodeCountSize(nodeArray); - int nodeffset = 0; - for (final PtNode ptNode : nodeArray.mData) { - ptNode.mCachedAddressBeforeUpdate = ptNode.mCachedAddressAfterUpdate = - nodeCountSize + nodeArrayOffset + nodeffset; - nodeffset += ptNode.mCachedSize; - } - nodeArrayOffset += nodeArray.mCachedSize; - } - return nodeArrayOffset; - } - - /** - * Updates the cached addresses of node arrays after recomputing their new positions. - * - * @param flatNodes the list of node arrays. - */ - private static void updatePtNodeArraysCachedAddresses(final ArrayList<PtNodeArray> flatNodes) { - for (final PtNodeArray nodeArray : flatNodes) { - nodeArray.mCachedAddressBeforeUpdate = nodeArray.mCachedAddressAfterUpdate; - for (final PtNode ptNode : nodeArray.mData) { - ptNode.mCachedAddressBeforeUpdate = ptNode.mCachedAddressAfterUpdate; - } - } - } - - /** - * Compute the cached parent addresses after all has been updated. - * - * The parent addresses are used by some binary formats at write-to-disk time. Not all formats - * need them. In particular, version 2 does not need them, and version 3 does. - * - * @param flatNodes the flat array of node arrays to fill in - */ - private static void computeParentAddresses(final ArrayList<PtNodeArray> flatNodes) { - for (final PtNodeArray nodeArray : flatNodes) { - for (final PtNode ptNode : nodeArray.mData) { - if (null != ptNode.mChildren) { - // Assign my address to children's parent address - // Here BeforeUpdate and AfterUpdate addresses have the same value, so it - // does not matter which we use. - ptNode.mChildren.mCachedParentAddress = ptNode.mCachedAddressAfterUpdate - - ptNode.mChildren.mCachedAddressAfterUpdate; - } - } - } - } - - /** - * Compute the addresses and sizes of an ordered list of PtNode arrays. - * - * This method takes a list of PtNode arrays and will update their cached address and size - * values so that they can be written into a file. It determines the smallest size each of the - * PtNode arrays can be given the addresses of its children and attributes, and store that into - * each PtNode. - * The order of the PtNode is given by the order of the array. This method makes no effort - * to find a good order; it only mechanically computes the size this order results in. - * - * @param dict the dictionary - * @param flatNodes the ordered list of PtNode arrays - * @param formatOptions file format options. - * @return the same array it was passed. The nodes have been updated for address and size. - */ - /* package */ static ArrayList<PtNodeArray> computeAddresses(final FusionDictionary dict, - final ArrayList<PtNodeArray> flatNodes, final FormatOptions formatOptions) { - // First get the worst possible sizes and offsets - for (final PtNodeArray n : flatNodes) calculatePtNodeArrayMaximumSize(n, formatOptions); - final int offset = initializePtNodeArraysCachedAddresses(flatNodes, formatOptions); - - MakedictLog.i("Compressing the array addresses. Original size : " + offset); - MakedictLog.i("(Recursively seen size : " + offset + ")"); - - int passes = 0; - boolean changesDone = false; - do { - changesDone = false; - int ptNodeArrayStartOffset = 0; - for (final PtNodeArray ptNodeArray : flatNodes) { - ptNodeArray.mCachedAddressAfterUpdate = ptNodeArrayStartOffset; - final int oldNodeArraySize = ptNodeArray.mCachedSize; - final boolean changed = - computeActualPtNodeArraySize(ptNodeArray, dict, formatOptions); - final int newNodeArraySize = ptNodeArray.mCachedSize; - if (oldNodeArraySize < newNodeArraySize) { - throw new RuntimeException("Increased size ?!"); - } - ptNodeArrayStartOffset += newNodeArraySize; - changesDone |= changed; - } - updatePtNodeArraysCachedAddresses(flatNodes); - ++passes; - if (passes > MAX_PASSES) throw new RuntimeException("Too many passes - probably a bug"); - } while (changesDone); - - if (formatOptions.mSupportsDynamicUpdate) { - computeParentAddresses(flatNodes); - } - final PtNodeArray lastPtNodeArray = flatNodes.get(flatNodes.size() - 1); - MakedictLog.i("Compression complete in " + passes + " passes."); - MakedictLog.i("After address compression : " - + (lastPtNodeArray.mCachedAddressAfterUpdate + lastPtNodeArray.mCachedSize)); - - return flatNodes; - } - - /** - * Sanity-checking method. - * - * This method checks a list of PtNode arrays for juxtaposition, that is, it will do - * nothing if each node array's cached address is actually the previous node array's address - * plus the previous node's size. - * If this is not the case, it will throw an exception. - * - * @param arrays the list of node arrays to check - */ - /* package */ static void checkFlatPtNodeArrayList(final ArrayList<PtNodeArray> arrays) { - int offset = 0; - int index = 0; - for (final PtNodeArray ptNodeArray : arrays) { - // BeforeUpdate and AfterUpdate addresses are the same here, so it does not matter - // which we use. - if (ptNodeArray.mCachedAddressAfterUpdate != offset) { - throw new RuntimeException("Wrong address for node " + index - + " : expected " + offset + ", got " + - ptNodeArray.mCachedAddressAfterUpdate); - } - ++index; - offset += ptNodeArray.mCachedSize; - } - } - - /** - * Helper method to write a children position to a file. - * - * @param buffer the buffer to write to. - * @param index the index in the buffer to write the address to. - * @param position the position to write. - * @return the size in bytes the address actually took. - */ - /* package */ static int writeChildrenPosition(final byte[] buffer, int index, - final int position) { - switch (getByteSize(position)) { - case 1: - buffer[index++] = (byte)position; - return 1; - case 2: - buffer[index++] = (byte)(0xFF & (position >> 8)); - buffer[index++] = (byte)(0xFF & position); - return 2; - case 3: - buffer[index++] = (byte)(0xFF & (position >> 16)); - buffer[index++] = (byte)(0xFF & (position >> 8)); - buffer[index++] = (byte)(0xFF & position); - return 3; - case 0: - return 0; - default: - throw new RuntimeException("Position " + position + " has a strange size"); - } - } - - /** - * Helper method to write a signed children position to a file. - * - * @param buffer the buffer to write to. - * @param index the index in the buffer to write the address to. - * @param position the position to write. - * @return the size in bytes the address actually took. - */ - /* package */ static int writeSignedChildrenPosition(final byte[] buffer, int index, - final int position) { - if (!BinaryDictIOUtils.hasChildrenAddress(position)) { - buffer[index] = buffer[index + 1] = buffer[index + 2] = 0; - } else { - final int absPosition = Math.abs(position); - buffer[index++] = - (byte)((position < 0 ? FormatSpec.MSB8 : 0) | (0xFF & (absPosition >> 16))); - buffer[index++] = (byte)(0xFF & (absPosition >> 8)); - buffer[index++] = (byte)(0xFF & absPosition); - } - return 3; - } - - /** - * Makes the flag value for a PtNode. - * - * @param hasMultipleChars whether the PtNode has multiple chars. - * @param isTerminal whether the PtNode is terminal. - * @param childrenAddressSize the size of a children address. - * @param hasShortcuts whether the PtNode has shortcuts. - * @param hasBigrams whether the PtNode has bigrams. - * @param isNotAWord whether the PtNode is not a word. - * @param isBlackListEntry whether the PtNode is a blacklist entry. - * @param formatOptions file format options. - * @return the flags - */ - static int makePtNodeFlags(final boolean hasMultipleChars, final boolean isTerminal, - final int childrenAddressSize, final boolean hasShortcuts, final boolean hasBigrams, - final boolean isNotAWord, final boolean isBlackListEntry, - final FormatOptions formatOptions) { - byte flags = 0; - if (hasMultipleChars) flags |= FormatSpec.FLAG_HAS_MULTIPLE_CHARS; - if (isTerminal) flags |= FormatSpec.FLAG_IS_TERMINAL; - if (formatOptions.mSupportsDynamicUpdate) { - flags |= FormatSpec.FLAG_IS_NOT_MOVED; - } else if (true) { - switch (childrenAddressSize) { - case 1: - flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE; - break; - case 2: - flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES; - break; - case 3: - flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES; - break; - case 0: - flags |= FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS; - break; - default: - throw new RuntimeException("Node with a strange address"); - } - } - if (hasShortcuts) flags |= FormatSpec.FLAG_HAS_SHORTCUT_TARGETS; - if (hasBigrams) flags |= FormatSpec.FLAG_HAS_BIGRAMS; - if (isNotAWord) flags |= FormatSpec.FLAG_IS_NOT_A_WORD; - if (isBlackListEntry) flags |= FormatSpec.FLAG_IS_BLACKLISTED; - return flags; - } - - /* package */ static byte makePtNodeFlags(final PtNode node, final int childrenOffset, - final FormatOptions formatOptions) { - return (byte) makePtNodeFlags(node.mChars.length > 1, node.mFrequency >= 0, - getByteSize(childrenOffset), - node.mShortcutTargets != null && !node.mShortcutTargets.isEmpty(), - node.mBigrams != null, node.mIsNotAWord, node.mIsBlacklistEntry, formatOptions); - } - - /** - * Makes the flag value for a bigram. - * - * @param more whether there are more bigrams after this one. - * @param offset the offset of the bigram. - * @param bigramFrequency the frequency of the bigram, 0..255. - * @param unigramFrequency the unigram frequency of the same word, 0..255. - * @param word the second bigram, for debugging purposes - * @return the flags - */ - /* package */ static final int makeBigramFlags(final boolean more, final int offset, - int bigramFrequency, final int unigramFrequency, final String word) { - int bigramFlags = (more ? FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT : 0) - + (offset < 0 ? FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE : 0); - switch (getByteSize(offset)) { - case 1: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE; - break; - case 2: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES; - break; - case 3: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES; - break; - default: - throw new RuntimeException("Strange offset size"); - } - if (unigramFrequency > bigramFrequency) { - MakedictLog.e("Unigram freq is superior to bigram freq for \"" + word - + "\". Bigram freq is " + bigramFrequency + ", unigram freq for " - + word + " is " + unigramFrequency); - bigramFrequency = unigramFrequency; - } - // We compute the difference between 255 (which means probability = 1) and the - // unigram score. We split this into a number of discrete steps. - // Now, the steps are numbered 0~15; 0 represents an increase of 1 step while 15 - // represents an increase of 16 steps: a value of 15 will be interpreted as the median - // value of the 16th step. In all justice, if the bigram frequency is low enough to be - // rounded below the first step (which means it is less than half a step higher than the - // unigram frequency) then the unigram frequency itself is the best approximation of the - // bigram freq that we could possibly supply, hence we should *not* include this bigram - // in the file at all. - // until this is done, we'll write 0 and slightly overestimate this case. - // In other words, 0 means "between 0.5 step and 1.5 step", 1 means "between 1.5 step - // and 2.5 steps", and 15 means "between 15.5 steps and 16.5 steps". So we want to - // divide our range [unigramFreq..MAX_TERMINAL_FREQUENCY] in 16.5 steps to get the - // step size. Then we compute the start of the first step (the one where value 0 starts) - // by adding half-a-step to the unigramFrequency. From there, we compute the integer - // number of steps to the bigramFrequency. One last thing: we want our steps to include - // their lower bound and exclude their higher bound so we need to have the first step - // start at exactly 1 unit higher than floor(unigramFreq + half a step). - // Note : to reconstruct the score, the dictionary reader will need to divide - // MAX_TERMINAL_FREQUENCY - unigramFreq by 16.5 likewise to get the value of the step, - // and add (discretizedFrequency + 0.5 + 0.5) times this value to get the best - // approximation. (0.5 to get the first step start, and 0.5 to get the middle of the - // step pointed by the discretized frequency. - final float stepSize = - (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency) - / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY); - final float firstStepStart = 1 + unigramFrequency + (stepSize / 2.0f); - final int discretizedFrequency = (int)((bigramFrequency - firstStepStart) / stepSize); - // If the bigram freq is less than half-a-step higher than the unigram freq, we get -1 - // here. The best approximation would be the unigram freq itself, so we should not - // include this bigram in the dictionary. For now, register as 0, and live with the - // small over-estimation that we get in this case. TODO: actually remove this bigram - // if discretizedFrequency < 0. - final int finalBigramFrequency = discretizedFrequency > 0 ? discretizedFrequency : 0; - bigramFlags += finalBigramFrequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY; - return bigramFlags; - } - - /** - * Makes the 2-byte value for options flags. - */ - private static final int makeOptionsValue(final FusionDictionary dictionary, - final FormatOptions formatOptions) { - final DictionaryOptions options = dictionary.mOptions; - final boolean hasBigrams = dictionary.hasBigrams(); - return (options.mFrenchLigatureProcessing ? FormatSpec.FRENCH_LIGATURE_PROCESSING_FLAG : 0) - + (options.mGermanUmlautProcessing ? FormatSpec.GERMAN_UMLAUT_PROCESSING_FLAG : 0) - + (hasBigrams ? FormatSpec.CONTAINS_BIGRAMS_FLAG : 0) - + (formatOptions.mSupportsDynamicUpdate ? FormatSpec.SUPPORTS_DYNAMIC_UPDATE : 0); - } - - /** - * Makes the flag value for a shortcut. - * - * @param more whether there are more attributes after this one. - * @param frequency the frequency of the attribute, 0..15 - * @return the flags - */ - static final int makeShortcutFlags(final boolean more, final int frequency) { - return (more ? FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT : 0) - + (frequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY); - } - - /* package */ static final int writeParentAddress(final byte[] buffer, final int index, - final int address, final FormatOptions formatOptions) { - if (BinaryDictIOUtils.supportsDynamicUpdate(formatOptions)) { - if (address == FormatSpec.NO_PARENT_ADDRESS) { - buffer[index] = buffer[index + 1] = buffer[index + 2] = 0; - } else { - final int absAddress = Math.abs(address); - assert(absAddress <= FormatSpec.SINT24_MAX); - buffer[index] = (byte)((address < 0 ? FormatSpec.MSB8 : 0) - | ((absAddress >> 16) & 0xFF)); - buffer[index + 1] = (byte)((absAddress >> 8) & 0xFF); - buffer[index + 2] = (byte)(absAddress & 0xFF); - } - return index + 3; - } else { - return index; - } - } - - /* package */ static final int getChildrenPosition(final PtNode ptNode, - final FormatOptions formatOptions) { - int positionOfChildrenPosField = ptNode.mCachedAddressAfterUpdate - + getNodeHeaderSize(ptNode, formatOptions); - if (ptNode.isTerminal()) { - // A terminal node has either the terminal id or the frequency. - // If positionOfChildrenPosField is incorrect, we may crash when jumping to the children - // position. - if (formatOptions.mHasTerminalId) { - positionOfChildrenPosField += FormatSpec.PTNODE_TERMINAL_ID_SIZE; - } else { - positionOfChildrenPosField += FormatSpec.PTNODE_FREQUENCY_SIZE; - } - } - return null == ptNode.mChildren ? FormatSpec.NO_CHILDREN_ADDRESS - : ptNode.mChildren.mCachedAddressAfterUpdate - positionOfChildrenPosField; - } - - /** - * Write a PtNodeArray. The PtNodeArray is expected to have its final position cached. - * - * @param dict the dictionary the node array is a part of (for relative offsets). - * @param dictEncoder the dictionary encoder. - * @param ptNodeArray the node array to write. - * @param formatOptions file format options. - */ - @SuppressWarnings("unused") - /* package */ static void writePlacedPtNodeArray(final FusionDictionary dict, - final DictEncoder dictEncoder, final PtNodeArray ptNodeArray, - final FormatOptions formatOptions) { - // TODO: Make the code in common with BinaryDictIOUtils#writePtNode - dictEncoder.setPosition(ptNodeArray.mCachedAddressAfterUpdate); - - final int ptNodeCount = ptNodeArray.mData.size(); - dictEncoder.writePtNodeCount(ptNodeCount); - final int parentPosition = - (ptNodeArray.mCachedParentAddress == FormatSpec.NO_PARENT_ADDRESS) - ? FormatSpec.NO_PARENT_ADDRESS - : ptNodeArray.mCachedParentAddress + ptNodeArray.mCachedAddressAfterUpdate; - for (int i = 0; i < ptNodeCount; ++i) { - final PtNode ptNode = ptNodeArray.mData.get(i); - if (dictEncoder.getPosition() != ptNode.mCachedAddressAfterUpdate) { - throw new RuntimeException("Bug: write index is not the same as the cached address " - + "of the node : " + dictEncoder.getPosition() + " <> " - + ptNode.mCachedAddressAfterUpdate); - } - // Sanity checks. - if (DBG && ptNode.mFrequency > FormatSpec.MAX_TERMINAL_FREQUENCY) { - throw new RuntimeException("A node has a frequency > " - + FormatSpec.MAX_TERMINAL_FREQUENCY - + " : " + ptNode.mFrequency); - } - dictEncoder.writePtNode(ptNode, parentPosition, formatOptions, dict); - } - if (formatOptions.mSupportsDynamicUpdate) { - dictEncoder.writeForwardLinkAddress(FormatSpec.NO_FORWARD_LINK_ADDRESS); - } - if (dictEncoder.getPosition() != ptNodeArray.mCachedAddressAfterUpdate - + ptNodeArray.mCachedSize) { - throw new RuntimeException("Not the same size : written " - + (dictEncoder.getPosition() - ptNodeArray.mCachedAddressAfterUpdate) - + " bytes from a node that should have " + ptNodeArray.mCachedSize + " bytes"); - } - } - - /** - * Dumps a collection of useful statistics about a list of PtNode arrays. - * - * This prints purely informative stuff, like the total estimated file size, the - * number of PtNode arrays, of PtNodes, the repartition of each address size, etc - * - * @param ptNodeArrays the list of PtNode arrays. - */ - /* package */ static void showStatistics(ArrayList<PtNodeArray> ptNodeArrays) { - int firstTerminalAddress = Integer.MAX_VALUE; - int lastTerminalAddress = Integer.MIN_VALUE; - int size = 0; - int ptNodes = 0; - int maxNodes = 0; - int maxRuns = 0; - for (final PtNodeArray ptNodeArray : ptNodeArrays) { - if (maxNodes < ptNodeArray.mData.size()) maxNodes = ptNodeArray.mData.size(); - for (final PtNode ptNode : ptNodeArray.mData) { - ++ptNodes; - if (ptNode.mChars.length > maxRuns) maxRuns = ptNode.mChars.length; - if (ptNode.mFrequency >= 0) { - if (ptNodeArray.mCachedAddressAfterUpdate < firstTerminalAddress) - firstTerminalAddress = ptNodeArray.mCachedAddressAfterUpdate; - if (ptNodeArray.mCachedAddressAfterUpdate > lastTerminalAddress) - lastTerminalAddress = ptNodeArray.mCachedAddressAfterUpdate; - } - } - if (ptNodeArray.mCachedAddressAfterUpdate + ptNodeArray.mCachedSize > size) { - size = ptNodeArray.mCachedAddressAfterUpdate + ptNodeArray.mCachedSize; - } - } - final int[] ptNodeCounts = new int[maxNodes + 1]; - final int[] runCounts = new int[maxRuns + 1]; - for (final PtNodeArray ptNodeArray : ptNodeArrays) { - ++ptNodeCounts[ptNodeArray.mData.size()]; - for (final PtNode ptNode : ptNodeArray.mData) { - ++runCounts[ptNode.mChars.length]; - } - } - - MakedictLog.i("Statistics:\n" - + " total file size " + size + "\n" - + " " + ptNodeArrays.size() + " node arrays\n" - + " " + ptNodes + " PtNodes (" + ((float)ptNodes / ptNodeArrays.size()) - + " PtNodes per node)\n" - + " first terminal at " + firstTerminalAddress + "\n" - + " last terminal at " + lastTerminalAddress + "\n" - + " PtNode stats : max = " + maxNodes); - for (int i = 0; i < ptNodeCounts.length; ++i) { - MakedictLog.i(" " + i + " : " + ptNodeCounts[i]); - } - MakedictLog.i(" Character run stats : max = " + maxRuns); - for (int i = 0; i < runCounts.length; ++i) { - MakedictLog.i(" " + i + " : " + runCounts[i]); - } - } - - /** - * Writes a file header to an output stream. - * - * @param destination the stream to write the file header to. - * @param dict the dictionary to write. - * @param formatOptions file format options. - * @return the size of the header. - */ - /* package */ static int writeDictionaryHeader(final OutputStream destination, - final FusionDictionary dict, final FormatOptions formatOptions) - throws IOException, UnsupportedFormatException { - final int version = formatOptions.mVersion; - if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION - || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) { - throw new UnsupportedFormatException("Requested file format version " + version - + ", but this implementation only supports versions " - + FormatSpec.MINIMUM_SUPPORTED_VERSION + " through " - + FormatSpec.MAXIMUM_SUPPORTED_VERSION); - } - - ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(256); - - // The magic number in big-endian order. - // Magic number for all versions. - headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 24))); - headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 16))); - headerBuffer.write((byte) (0xFF & (FormatSpec.MAGIC_NUMBER >> 8))); - headerBuffer.write((byte) (0xFF & FormatSpec.MAGIC_NUMBER)); - // Dictionary version. - headerBuffer.write((byte) (0xFF & (version >> 8))); - headerBuffer.write((byte) (0xFF & version)); - - // Options flags - final int options = makeOptionsValue(dict, formatOptions); - headerBuffer.write((byte) (0xFF & (options >> 8))); - headerBuffer.write((byte) (0xFF & options)); - final int headerSizeOffset = headerBuffer.size(); - // Placeholder to be written later with header size. - for (int i = 0; i < 4; ++i) { - headerBuffer.write(0); - } - // Write out the options. - for (final String key : dict.mOptions.mAttributes.keySet()) { - final String value = dict.mOptions.mAttributes.get(key); - CharEncoding.writeString(headerBuffer, key); - CharEncoding.writeString(headerBuffer, value); - } - final int size = headerBuffer.size(); - final byte[] bytes = headerBuffer.toByteArray(); - // Write out the header size. - bytes[headerSizeOffset] = (byte) (0xFF & (size >> 24)); - bytes[headerSizeOffset + 1] = (byte) (0xFF & (size >> 16)); - bytes[headerSizeOffset + 2] = (byte) (0xFF & (size >> 8)); - bytes[headerSizeOffset + 3] = (byte) (0xFF & (size >> 0)); - destination.write(bytes); - - headerBuffer.close(); - return size; - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java deleted file mode 100644 index d5516ef46..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java +++ /dev/null @@ -1,599 +0,0 @@ -/* - * Copyright (C) 2012 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.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; -import com.android.inputmethod.latin.utils.ByteArrayDictBuffer; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.Map; -import java.util.Stack; - -public final class BinaryDictIOUtils { - private static final boolean DBG = false; - - private BinaryDictIOUtils() { - // This utility class is not publicly instantiable. - } - - private static final class Position { - public static final int NOT_READ_PTNODE_COUNT = -1; - - public int mAddress; - public int mNumOfPtNode; - public int mPosition; - public int mLength; - - public Position(int address, int length) { - mAddress = address; - mLength = length; - mNumOfPtNode = NOT_READ_PTNODE_COUNT; - } - } - - /** - * Retrieves all node arrays without recursive call. - */ - private static void readUnigramsAndBigramsBinaryInner(final DictDecoder dictDecoder, - final int headerSize, final Map<Integer, String> words, - final Map<Integer, Integer> frequencies, - final Map<Integer, ArrayList<PendingAttribute>> bigrams, - final FormatOptions formatOptions) { - int[] pushedChars = new int[FormatSpec.MAX_WORD_LENGTH + 1]; - - Stack<Position> stack = new Stack<Position>(); - int index = 0; - - Position initPos = new Position(headerSize, 0); - stack.push(initPos); - - while (!stack.empty()) { - Position p = stack.peek(); - - if (DBG) { - MakedictLog.d("read: address=" + p.mAddress + ", numOfPtNode=" + - p.mNumOfPtNode + ", position=" + p.mPosition + ", length=" + p.mLength); - } - - if (dictDecoder.getPosition() != p.mAddress) dictDecoder.setPosition(p.mAddress); - if (index != p.mLength) index = p.mLength; - - if (p.mNumOfPtNode == Position.NOT_READ_PTNODE_COUNT) { - p.mNumOfPtNode = dictDecoder.readPtNodeCount(); - p.mAddress += getPtNodeCountSize(p.mNumOfPtNode); - p.mPosition = 0; - } - if (p.mNumOfPtNode == 0) { - stack.pop(); - continue; - } - PtNodeInfo info = dictDecoder.readPtNode(p.mAddress, formatOptions); - for (int i = 0; i < info.mCharacters.length; ++i) { - pushedChars[index++] = info.mCharacters[i]; - } - p.mPosition++; - - final boolean isMovedPtNode = isMovedPtNode(info.mFlags, - formatOptions); - final boolean isDeletedPtNode = isDeletedPtNode(info.mFlags, - formatOptions); - if (!isMovedPtNode && !isDeletedPtNode - && info.mFrequency != FusionDictionary.PtNode.NOT_A_TERMINAL) {// found word - words.put(info.mOriginalAddress, new String(pushedChars, 0, index)); - frequencies.put(info.mOriginalAddress, info.mFrequency); - if (info.mBigrams != null) bigrams.put(info.mOriginalAddress, info.mBigrams); - } - - if (p.mPosition == p.mNumOfPtNode) { - if (formatOptions.mSupportsDynamicUpdate) { - final boolean hasValidForwardLinkAddress = - dictDecoder.readAndFollowForwardLink(); - if (hasValidForwardLinkAddress && dictDecoder.hasNextPtNodeArray()) { - // The node array has a forward link. - p.mNumOfPtNode = Position.NOT_READ_PTNODE_COUNT; - p.mAddress = dictDecoder.getPosition(); - } else { - stack.pop(); - } - } else { - stack.pop(); - } - } else { - // The Ptnode array has more PtNodes. - p.mAddress = dictDecoder.getPosition(); - } - - if (!isMovedPtNode && hasChildrenAddress(info.mChildrenAddress)) { - final Position childrenPos = new Position(info.mChildrenAddress, index); - stack.push(childrenPos); - } - } - } - - /** - * Reads unigrams and bigrams from the binary file. - * Doesn't store a full memory representation of the dictionary. - * - * @param dictDecoder the dict decoder. - * @param words the map to store the address as a key and the word as a value. - * @param frequencies the map to store the address as a key and the frequency as a value. - * @param bigrams the map to store the address as a key and the list of address as a value. - * @throws IOException if the file can't be read. - * @throws UnsupportedFormatException if the format of the file is not recognized. - */ - /* package */ static void readUnigramsAndBigramsBinary(final DictDecoder dictDecoder, - final Map<Integer, String> words, final Map<Integer, Integer> frequencies, - final Map<Integer, ArrayList<PendingAttribute>> bigrams) throws IOException, - UnsupportedFormatException { - // Read header - final FileHeader header = dictDecoder.readHeader(); - readUnigramsAndBigramsBinaryInner(dictDecoder, header.mHeaderSize, words, - frequencies, bigrams, header.mFormatOptions); - } - - /** - * Gets the address of the last PtNode of the exact matching word in the dictionary. - * If no match is found, returns NOT_VALID_WORD. - * - * @param dictDecoder the dict decoder. - * @param word the word we search for. - * @return the address of the terminal node. - * @throws IOException if the file can't be read. - * @throws UnsupportedFormatException if the format of the file is not recognized. - */ - @UsedForTesting - /* package */ static int getTerminalPosition(final DictDecoder dictDecoder, - final String word) throws IOException, UnsupportedFormatException { - if (word == null) return FormatSpec.NOT_VALID_WORD; - dictDecoder.setPosition(0); - - final FileHeader header = dictDecoder.readHeader(); - int wordPos = 0; - final int wordLen = word.codePointCount(0, word.length()); - for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) { - if (wordPos >= wordLen) return FormatSpec.NOT_VALID_WORD; - - do { - final int ptNodeCount = dictDecoder.readPtNodeCount(); - boolean foundNextPtNode = false; - for (int i = 0; i < ptNodeCount; ++i) { - final int ptNodePos = dictDecoder.getPosition(); - final PtNodeInfo currentInfo = dictDecoder.readPtNode(ptNodePos, - header.mFormatOptions); - final boolean isMovedNode = isMovedPtNode(currentInfo.mFlags, - header.mFormatOptions); - final boolean isDeletedNode = isDeletedPtNode(currentInfo.mFlags, - header.mFormatOptions); - if (isMovedNode) continue; - boolean same = true; - for (int p = 0, j = word.offsetByCodePoints(0, wordPos); - p < currentInfo.mCharacters.length; - ++p, j = word.offsetByCodePoints(j, 1)) { - if (wordPos + p >= wordLen - || word.codePointAt(j) != currentInfo.mCharacters[p]) { - same = false; - break; - } - } - - if (same) { - // found the PtNode matches the word. - if (wordPos + currentInfo.mCharacters.length == wordLen) { - if (currentInfo.mFrequency == PtNode.NOT_A_TERMINAL - || isDeletedNode) { - return FormatSpec.NOT_VALID_WORD; - } else { - return ptNodePos; - } - } - wordPos += currentInfo.mCharacters.length; - if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) { - return FormatSpec.NOT_VALID_WORD; - } - foundNextPtNode = true; - dictDecoder.setPosition(currentInfo.mChildrenAddress); - break; - } - } - - // If we found the next PtNode, it is under the file pointer. - // But if not, we are at the end of this node array so we expect to have - // a forward link address that we need to consult and possibly resume - // search on the next node array in the linked list. - if (foundNextPtNode) break; - if (!header.mFormatOptions.mSupportsDynamicUpdate) { - return FormatSpec.NOT_VALID_WORD; - } - - final boolean hasValidForwardLinkAddress = - dictDecoder.readAndFollowForwardLink(); - if (!hasValidForwardLinkAddress || !dictDecoder.hasNextPtNodeArray()) { - return FormatSpec.NOT_VALID_WORD; - } - } while(true); - } - return FormatSpec.NOT_VALID_WORD; - } - - /** - * @return the size written, in bytes. Always 3 bytes. - */ - static int writeSInt24ToBuffer(final DictBuffer dictBuffer, - final int value) { - final int absValue = Math.abs(value); - dictBuffer.put((byte)(((value < 0 ? 0x80 : 0) | (absValue >> 16)) & 0xFF)); - dictBuffer.put((byte)((absValue >> 8) & 0xFF)); - dictBuffer.put((byte)(absValue & 0xFF)); - return 3; - } - - /** - * @return the size written, in bytes. Always 3 bytes. - */ - static int writeSInt24ToStream(final OutputStream destination, final int value) - throws IOException { - final int absValue = Math.abs(value); - destination.write((byte)(((value < 0 ? 0x80 : 0) | (absValue >> 16)) & 0xFF)); - destination.write((byte)((absValue >> 8) & 0xFF)); - destination.write((byte)(absValue & 0xFF)); - return 3; - } - - /** - * @return the size written, in bytes. 1, 2, or 3 bytes. - */ - private static int writeVariableAddress(final OutputStream destination, final int value) - throws IOException { - switch (BinaryDictEncoderUtils.getByteSize(value)) { - case 1: - destination.write((byte)value); - break; - case 2: - destination.write((byte)(0xFF & (value >> 8))); - destination.write((byte)(0xFF & value)); - break; - case 3: - destination.write((byte)(0xFF & (value >> 16))); - destination.write((byte)(0xFF & (value >> 8))); - destination.write((byte)(0xFF & value)); - break; - } - return BinaryDictEncoderUtils.getByteSize(value); - } - - static void skipString(final DictBuffer dictBuffer, - final boolean hasMultipleChars) { - if (hasMultipleChars) { - int character = CharEncoding.readChar(dictBuffer); - while (character != FormatSpec.INVALID_CHARACTER) { - character = CharEncoding.readChar(dictBuffer); - } - } else { - CharEncoding.readChar(dictBuffer); - } - } - - /** - * Write a string to a stream. - * - * @param destination the stream to write. - * @param word the string to be written. - * @return the size written, in bytes. - * @throws IOException - */ - private static int writeString(final OutputStream destination, final String word) - throws IOException { - int size = 0; - final int length = word.length(); - for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) { - final int codePoint = word.codePointAt(i); - if (CharEncoding.getCharSize(codePoint) == 1) { - destination.write((byte)codePoint); - size++; - } else { - destination.write((byte)(0xFF & (codePoint >> 16))); - destination.write((byte)(0xFF & (codePoint >> 8))); - destination.write((byte)(0xFF & codePoint)); - size += 3; - } - } - destination.write((byte)FormatSpec.PTNODE_CHARACTERS_TERMINATOR); - size += FormatSpec.PTNODE_TERMINATOR_SIZE; - return size; - } - - /** - * Write a PtNode to an output stream from a PtNodeInfo. - * A PtNode is an in-memory representation of a node in the patricia trie. - * A PtNode info is a container for low-level information about how the - * PtNode is stored in the binary format. - * - * @param destination the stream to write. - * @param info the PtNode info to be written. - * @return the size written, in bytes. - */ - private static int writePtNode(final OutputStream destination, final PtNodeInfo info) - throws IOException { - int size = FormatSpec.PTNODE_FLAGS_SIZE; - destination.write((byte)info.mFlags); - final int parentOffset = info.mParentAddress == FormatSpec.NO_PARENT_ADDRESS ? - FormatSpec.NO_PARENT_ADDRESS : info.mParentAddress - info.mOriginalAddress; - size += writeSInt24ToStream(destination, parentOffset); - - for (int i = 0; i < info.mCharacters.length; ++i) { - if (CharEncoding.getCharSize(info.mCharacters[i]) == 1) { - destination.write((byte)info.mCharacters[i]); - size++; - } else { - size += writeSInt24ToStream(destination, info.mCharacters[i]); - } - } - if (info.mCharacters.length > 1) { - destination.write((byte)FormatSpec.PTNODE_CHARACTERS_TERMINATOR); - size++; - } - - if ((info.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0) { - destination.write((byte)info.mFrequency); - size++; - } - - if (DBG) { - MakedictLog.d("writePtNode origin=" + info.mOriginalAddress + ", size=" + size - + ", child=" + info.mChildrenAddress + ", characters =" - + new String(info.mCharacters, 0, info.mCharacters.length)); - } - final int childrenOffset = info.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS ? - 0 : info.mChildrenAddress - (info.mOriginalAddress + size); - writeSInt24ToStream(destination, childrenOffset); - size += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE; - - if (info.mShortcutTargets != null && info.mShortcutTargets.size() > 0) { - final int shortcutListSize = - BinaryDictEncoderUtils.getShortcutListSize(info.mShortcutTargets); - destination.write((byte)(shortcutListSize >> 8)); - destination.write((byte)(shortcutListSize & 0xFF)); - size += 2; - final Iterator<WeightedString> shortcutIterator = info.mShortcutTargets.iterator(); - while (shortcutIterator.hasNext()) { - final WeightedString target = shortcutIterator.next(); - destination.write((byte)BinaryDictEncoderUtils.makeShortcutFlags( - shortcutIterator.hasNext(), target.mFrequency)); - size++; - size += writeString(destination, target.mWord); - } - } - - if (info.mBigrams != null) { - // TODO: Consolidate this code with the code that computes the size of the bigram list - // in BinaryDictEncoderUtils#computeActualNodeArraySize - for (int i = 0; i < info.mBigrams.size(); ++i) { - - final int bigramFrequency = info.mBigrams.get(i).mFrequency; - int bigramFlags = (i < info.mBigrams.size() - 1) - ? FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT : 0; - size++; - final int bigramOffset = info.mBigrams.get(i).mAddress - (info.mOriginalAddress - + size); - bigramFlags |= (bigramOffset < 0) ? FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE : 0; - switch (BinaryDictEncoderUtils.getByteSize(bigramOffset)) { - case 1: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE; - break; - case 2: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES; - break; - case 3: - bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES; - break; - } - bigramFlags |= bigramFrequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY; - destination.write((byte)bigramFlags); - size += writeVariableAddress(destination, Math.abs(bigramOffset)); - } - } - return size; - } - - /** - * Compute the size of the PtNode. - */ - static int computePtNodeSize(final PtNodeInfo info, final FormatOptions formatOptions) { - int size = FormatSpec.PTNODE_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE - + BinaryDictEncoderUtils.getPtNodeCharactersSize(info.mCharacters) - + getChildrenAddressSize(info.mFlags, formatOptions); - if ((info.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0) { - size += FormatSpec.PTNODE_FREQUENCY_SIZE; - } - if (info.mShortcutTargets != null && !info.mShortcutTargets.isEmpty()) { - size += BinaryDictEncoderUtils.getShortcutListSize(info.mShortcutTargets); - } - if (info.mBigrams != null) { - for (final PendingAttribute attr : info.mBigrams) { - size += FormatSpec.PTNODE_FLAGS_SIZE; - size += BinaryDictEncoderUtils.getByteSize(attr.mAddress); - } - } - return size; - } - - /** - * Write a node array to the stream. - * - * @param destination the stream to write. - * @param infos an array of PtNodeInfo to be written. - * @return the size written, in bytes. - * @throws IOException - */ - static int writeNodes(final OutputStream destination, final PtNodeInfo[] infos) - throws IOException { - int size = getPtNodeCountSize(infos.length); - switch (getPtNodeCountSize(infos.length)) { - case 1: - destination.write((byte)infos.length); - break; - case 2: - final int encodedPtNodeCount = - infos.length | FormatSpec.LARGE_PTNODE_ARRAY_SIZE_FIELD_SIZE_FLAG; - destination.write((byte)(encodedPtNodeCount >> 8)); - destination.write((byte)(encodedPtNodeCount & 0xFF)); - break; - default: - throw new RuntimeException("Invalid node count size."); - } - for (final PtNodeInfo info : infos) size += writePtNode(destination, info); - writeSInt24ToStream(destination, FormatSpec.NO_FORWARD_LINK_ADDRESS); - return size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE; - } - - private static final int HEADER_READING_BUFFER_SIZE = 16384; - /** - * Convenience method to read the header of a binary file. - * - * This is quite resource intensive - don't call when performance is critical. - * - * @param file The file to read. - * @param offset The offset in the file where to start reading the data. - * @param length The length of the data file. - */ - private static FileHeader getDictionaryFileHeader( - final File file, final long offset, final long length) - throws FileNotFoundException, IOException, UnsupportedFormatException { - final byte[] buffer = new byte[HEADER_READING_BUFFER_SIZE]; - final DictDecoder dictDecoder = FormatSpec.getDictDecoder(file, - new DictDecoder.DictionaryBufferFactory() { - @Override - public DictBuffer getDictionaryBuffer(File file) - throws FileNotFoundException, IOException { - final FileInputStream inStream = new FileInputStream(file); - try { - inStream.skip(offset); - inStream.read(buffer); - return new ByteArrayDictBuffer(buffer); - } finally { - inStream.close(); - } - } - } - ); - return dictDecoder.readHeader(); - } - - public static FileHeader getDictionaryFileHeaderOrNull(final File file, final long offset, - final long length) { - try { - final FileHeader header = getDictionaryFileHeader(file, offset, length); - return header; - } catch (UnsupportedFormatException e) { - return null; - } catch (IOException e) { - return null; - } - } - - /** - * Helper method to hide the actual value of the no children address. - */ - public static boolean hasChildrenAddress(final int address) { - return FormatSpec.NO_CHILDREN_ADDRESS != address; - } - - /** - * Helper method to check whether the node is moved. - */ - public static boolean isMovedPtNode(final int flags, final FormatOptions options) { - return options.mSupportsDynamicUpdate - && ((flags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) == FormatSpec.FLAG_IS_MOVED); - } - - /** - * Helper method to check whether the dictionary can be updated dynamically. - */ - public static boolean supportsDynamicUpdate(final FormatOptions options) { - return options.mVersion >= FormatSpec.FIRST_VERSION_WITH_DYNAMIC_UPDATE - && options.mSupportsDynamicUpdate; - } - - /** - * Helper method to check whether the node is deleted. - */ - public static boolean isDeletedPtNode(final int flags, final FormatOptions formatOptions) { - return formatOptions.mSupportsDynamicUpdate - && ((flags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) == FormatSpec.FLAG_IS_DELETED); - } - - /** - * Compute the binary size of the node count - * @param count the node count - * @return the size of the node count, either 1 or 2 bytes. - */ - public static int getPtNodeCountSize(final int count) { - if (FormatSpec.MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT >= count) { - return 1; - } else if (FormatSpec.MAX_PTNODES_IN_A_PT_NODE_ARRAY >= count) { - return 2; - } else { - throw new RuntimeException("Can't have more than " - + FormatSpec.MAX_PTNODES_IN_A_PT_NODE_ARRAY + " PtNode in a PtNodeArray (found " - + count + ")"); - } - } - - static int getChildrenAddressSize(final int optionFlags, - final FormatOptions formatOptions) { - if (formatOptions.mSupportsDynamicUpdate) return FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE; - switch (optionFlags & FormatSpec.MASK_CHILDREN_ADDRESS_TYPE) { - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE: - return 1; - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES: - return 2; - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES: - return 3; - case FormatSpec.FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS: - default: - return 0; - } - } - - /** - * Calculate bigram frequency from compressed value - * - * @param unigramFrequency - * @param bigramFrequency compressed frequency - * @return approximate bigram frequency - */ - public static int reconstructBigramFrequency(final int unigramFrequency, - final int bigramFrequency) { - final float stepSize = (FormatSpec.MAX_TERMINAL_FREQUENCY - unigramFrequency) - / (1.5f + FormatSpec.MAX_BIGRAM_FREQUENCY); - final float resultFreqFloat = unigramFrequency + stepSize * (bigramFrequency + 1.0f); - return (int)resultFreqFloat; - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java deleted file mode 100644 index 3dbeee099..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.utils.ByteArrayDictBuffer; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.TreeMap; - -/** - * An interface of binary dictionary decoders. - */ -public interface DictDecoder { - - /** - * Reads and returns the file header. - */ - public FileHeader readHeader() throws IOException, UnsupportedFormatException; - - /** - * Reads PtNode from nodeAddress. - * @param ptNodePos the position of PtNode. - * @param formatOptions the format options. - * @return PtNodeInfo. - */ - public PtNodeInfo readPtNode(final int ptNodePos, final FormatOptions formatOptions); - - /** - * Reads a buffer and returns the memory representation of the dictionary. - * - * This high-level method takes a buffer and reads its contents, populating a - * FusionDictionary structure. The optional dict argument is an existing dictionary to - * which words from the buffer should be added. If it is null, a new dictionary is created. - * - * @param dict an optional dictionary to add words to, or null. - * @param deleteDictIfBroken a flag indicating whether this method should remove the broken - * dictionary or not. - * @return the created (or merged) dictionary. - */ - @UsedForTesting - public FusionDictionary readDictionaryBinary(final FusionDictionary dict, - final boolean deleteDictIfBroken) - throws FileNotFoundException, IOException, UnsupportedFormatException; - - /** - * Gets the address of the last PtNode of the exact matching word in the dictionary. - * If no match is found, returns NOT_VALID_WORD. - * - * @param word the word we search for. - * @return the address of the terminal node. - * @throws IOException if the file can't be read. - * @throws UnsupportedFormatException if the format of the file is not recognized. - */ - @UsedForTesting - public int getTerminalPosition(final String word) - throws IOException, UnsupportedFormatException; - - /** - * Reads unigrams and bigrams from the binary file. - * Doesn't store a full memory representation of the dictionary. - * - * @param words the map to store the address as a key and the word as a value. - * @param frequencies the map to store the address as a key and the frequency as a value. - * @param bigrams the map to store the address as a key and the list of address as a value. - * @throws IOException if the file can't be read. - * @throws UnsupportedFormatException if the format of the file is not recognized. - */ - @UsedForTesting - public void readUnigramsAndBigramsBinary(final TreeMap<Integer, String> words, - final TreeMap<Integer, Integer> frequencies, - final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams) - throws IOException, UnsupportedFormatException; - - /** - * Sets the position of the buffer to the given value. - * - * @param newPos the new position - */ - public void setPosition(final int newPos); - - /** - * Gets the position of the buffer. - * - * @return the position - */ - public int getPosition(); - - /** - * Reads and returns the PtNode count out of a buffer and forwards the pointer. - */ - public int readPtNodeCount(); - - /** - * Reads the forward link and advances the position. - * - * @return true if this method moves the file pointer, false otherwise. - */ - public boolean readAndFollowForwardLink(); - public boolean hasNextPtNodeArray(); - - /** - * Opens the dictionary file and makes DictBuffer. - */ - @UsedForTesting - public void openDictBuffer() throws FileNotFoundException, IOException; - @UsedForTesting - public boolean isDictBufferOpen(); - - // Constants for DictionaryBufferFactory. - public static final int USE_READONLY_BYTEBUFFER = 0x01000000; - public static final int USE_BYTEARRAY = 0x02000000; - public static final int USE_WRITABLE_BYTEBUFFER = 0x03000000; - public static final int MASK_DICTBUFFER = 0x0F000000; - - public interface DictionaryBufferFactory { - public DictBuffer getDictionaryBuffer(final File file) - throws FileNotFoundException, IOException; - } - - /** - * Creates DictionaryBuffer using a ByteBuffer - * - * This class uses less memory than DictionaryBufferFromByteArrayFactory, - * but doesn't perform as fast. - * When operating on a big dictionary, this class is preferred. - */ - public static final class DictionaryBufferFromReadOnlyByteBufferFactory - implements DictionaryBufferFactory { - @Override - public DictBuffer getDictionaryBuffer(final File file) - throws FileNotFoundException, IOException { - FileInputStream inStream = null; - ByteBuffer buffer = null; - try { - inStream = new FileInputStream(file); - buffer = inStream.getChannel().map(FileChannel.MapMode.READ_ONLY, - 0, file.length()); - } finally { - if (inStream != null) { - inStream.close(); - } - } - if (buffer != null) { - return new BinaryDictDecoderUtils.ByteBufferDictBuffer(buffer); - } - return null; - } - } - - /** - * Creates DictionaryBuffer using a byte array - * - * This class performs faster than other classes, but consumes more memory. - * When operating on a small dictionary, this class is preferred. - */ - public static final class DictionaryBufferFromByteArrayFactory - implements DictionaryBufferFactory { - @Override - public DictBuffer getDictionaryBuffer(final File file) - throws FileNotFoundException, IOException { - FileInputStream inStream = null; - try { - inStream = new FileInputStream(file); - final byte[] array = new byte[(int) file.length()]; - inStream.read(array); - return new ByteArrayDictBuffer(array); - } finally { - if (inStream != null) { - inStream.close(); - } - } - } - } - - /** - * Creates DictionaryBuffer using a writable ByteBuffer and a RandomAccessFile. - * - * This class doesn't perform as fast as other classes, - * but this class is the only option available for destructive operations (insert or delete) - * on a dictionary. - */ - @UsedForTesting - public static final class DictionaryBufferFromWritableByteBufferFactory - implements DictionaryBufferFactory { - @Override - public DictBuffer getDictionaryBuffer(final File file) - throws FileNotFoundException, IOException { - RandomAccessFile raFile = null; - ByteBuffer buffer = null; - try { - raFile = new RandomAccessFile(file, "rw"); - buffer = raFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, file.length()); - } finally { - if (raFile != null) { - raFile.close(); - } - } - if (buffer != null) { - return new BinaryDictDecoderUtils.ByteBufferDictBuffer(buffer); - } - return null; - } - } - - public void skipPtNode(final FormatOptions formatOptions); -} diff --git a/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java deleted file mode 100644 index ea5d492d8..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; - -import java.io.IOException; - -/** - * An interface of binary dictionary encoder. - */ -public interface DictEncoder { - public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions) - throws IOException, UnsupportedFormatException; - - public void setPosition(final int position); - public int getPosition(); - public void writePtNodeCount(final int ptNodeCount); - public void writeForwardLinkAddress(final int forwardLinkAddress); - - public void writePtNode(final PtNode ptNode, final int parentPosition, - final FormatOptions formatOptions, final FusionDictionary dict); -} diff --git a/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java deleted file mode 100644 index c4f7ec91f..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/DictUpdater.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.IOException; -import java.util.ArrayList; - -/** - * An interface of a binary dictionary updater. - */ -@UsedForTesting -public interface DictUpdater extends DictDecoder { - - /** - * Deletes the word from the binary dictionary. - * - * @param word the word to be deleted. - */ - @UsedForTesting - public void deleteWord(final String word) throws IOException, UnsupportedFormatException; - - /** - * Inserts a word into a binary dictionary. - * - * @param word the word to be inserted. - * @param frequency the frequency of the new word. - * @param bigramStrings bigram list, or null if none. - * @param shortcuts shortcut list, or null if none. - * @param isBlackListEntry whether this should be a blacklist entry. - */ - // TODO: Support batch insertion. - @UsedForTesting - public void insertWord(final String word, final int frequency, - final ArrayList<WeightedString> bigramStrings, - final ArrayList<WeightedString> shortcuts, final boolean isNotAWord, - final boolean isBlackListEntry) throws IOException, UnsupportedFormatException; -} diff --git a/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java b/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java new file mode 100644 index 000000000..df447fd75 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/DictionaryHeader.java @@ -0,0 +1,89 @@ +/* + * 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.makedict; + +import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions; +import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; + +/** + * Class representing dictionary header. + */ +public final class DictionaryHeader { + public final int mBodyOffset; + public final DictionaryOptions mDictionaryOptions; + public final FormatOptions mFormatOptions; + + // Note that these are corresponding definitions in native code in latinime::HeaderPolicy + // and latinime::HeaderReadWriteUtils. + // TODO: Standardize the key names and bump up the format version, taking care not to + // break format version 2 dictionaries. + public static final String DICTIONARY_VERSION_KEY = "version"; + public static final String DICTIONARY_LOCALE_KEY = "locale"; + public static final String DICTIONARY_ID_KEY = "dictionary"; + public static final String DICTIONARY_DESCRIPTION_KEY = "description"; + 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 DictionaryHeader(final int headerSize, final DictionaryOptions dictionaryOptions, + final FormatOptions formatOptions) throws UnsupportedFormatException { + mDictionaryOptions = dictionaryOptions; + mFormatOptions = formatOptions; + mBodyOffset = formatOptions.mVersion < FormatSpec.VERSION4 ? headerSize : 0; + if (null == getLocaleString()) { + throw new UnsupportedFormatException("Cannot create a FileHeader without a locale"); + } + if (null == getVersion()) { + throw new UnsupportedFormatException( + "Cannot create a FileHeader without a version"); + } + if (null == getId()) { + throw new UnsupportedFormatException("Cannot create a FileHeader without an ID"); + } + } + + // Helper method to get the locale as a String + public String getLocaleString() { + return mDictionaryOptions.mAttributes.get(DICTIONARY_LOCALE_KEY); + } + + // Helper method to get the version String + public String getVersion() { + return mDictionaryOptions.mAttributes.get(DICTIONARY_VERSION_KEY); + } + + // Helper method to get the dictionary ID as a String + public String getId() { + return mDictionaryOptions.mAttributes.get(DICTIONARY_ID_KEY); + } + + // Helper method to get the description + public String getDescription() { + // TODO: Right now each dictionary file comes with a description in its own language. + // It will display as is no matter the device's locale. It should be internationalized. + return mDictionaryOptions.mAttributes.get(DICTIONARY_DESCRIPTION_KEY); + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java deleted file mode 100644 index 28da9ffdd..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; - -/** - * The utility class to help dynamic updates on the binary dictionary. - * - * All the methods in this class are static. - */ -@UsedForTesting -public final class DynamicBinaryDictIOUtils { - private static final boolean DBG = false; - private static final int MAX_JUMPS = 10000; - - private DynamicBinaryDictIOUtils() { - // This utility class is not publicly instantiable. - } - - /* package */ static int markAsDeleted(final int flags) { - return (flags & (~FormatSpec.MASK_CHILDREN_ADDRESS_TYPE)) | FormatSpec.FLAG_IS_DELETED; - } - - /** - * Update a parent address in a PtNode that is referred to by ptNodeOriginAddress. - * - * @param dictUpdater the DictUpdater to write. - * @param ptNodeOriginAddress the address of the PtNode. - * @param newParentAddress the absolute address of the parent. - * @param formatOptions file format options. - */ - private static void updateParentAddress(final Ver3DictUpdater dictUpdater, - final int ptNodeOriginAddress, final int newParentAddress, - final FormatOptions formatOptions) { - final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); - final int originalPosition = dictBuffer.position(); - dictBuffer.position(ptNodeOriginAddress); - if (!formatOptions.mSupportsDynamicUpdate) { - throw new RuntimeException("this file format does not support parent addresses"); - } - final int flags = dictBuffer.readUnsignedByte(); - if (BinaryDictIOUtils.isMovedPtNode(flags, formatOptions)) { - // If the node is moved, the parent address is stored in the destination node. - // We are guaranteed to process the destination node later, so there is no need to - // update anything here. - dictBuffer.position(originalPosition); - return; - } - if (DBG) { - MakedictLog.d("update parent address flags=" + flags + ", " + ptNodeOriginAddress); - } - final int parentOffset = newParentAddress - ptNodeOriginAddress; - BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, parentOffset); - dictBuffer.position(originalPosition); - } - - /** - * Update parent addresses in a node array stored at ptNodeOriginAddress. - * - * @param dictUpdater the DictUpdater to be modified. - * @param ptNodeOriginAddress the address of the node array to update. - * @param newParentAddress the address to be written. - * @param formatOptions file format options. - */ - private static void updateParentAddresses(final Ver3DictUpdater dictUpdater, - final int ptNodeOriginAddress, final int newParentAddress, - final FormatOptions formatOptions) { - final int originalPosition = dictUpdater.getPosition(); - dictUpdater.setPosition(ptNodeOriginAddress); - do { - final int count = dictUpdater.readPtNodeCount(); - for (int i = 0; i < count; ++i) { - updateParentAddress(dictUpdater, dictUpdater.getPosition(), newParentAddress, - formatOptions); - dictUpdater.skipPtNode(formatOptions); - } - if (!dictUpdater.readAndFollowForwardLink()) break; - if (dictUpdater.getPosition() == FormatSpec.NO_FORWARD_LINK_ADDRESS) break; - } while (formatOptions.mSupportsDynamicUpdate); - dictUpdater.setPosition(originalPosition); - } - - /** - * Update a children address in a PtNode that is addressed by ptNodeOriginAddress. - * - * @param dictUpdater the DictUpdater to write. - * @param ptNodeOriginAddress the address of the PtNode. - * @param newChildrenAddress the absolute address of the child. - * @param formatOptions file format options. - */ - private static void updateChildrenAddress(final Ver3DictUpdater dictUpdater, - final int ptNodeOriginAddress, final int newChildrenAddress, - final FormatOptions formatOptions) { - final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); - final int originalPosition = dictBuffer.position(); - dictBuffer.position(ptNodeOriginAddress); - final int flags = dictBuffer.readUnsignedByte(); - BinaryDictDecoderUtils.readParentAddress(dictBuffer, formatOptions); - BinaryDictIOUtils.skipString(dictBuffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0); - if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) dictBuffer.readUnsignedByte(); - final int childrenOffset = newChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS - ? FormatSpec.NO_CHILDREN_ADDRESS : newChildrenAddress - dictBuffer.position(); - BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, childrenOffset); - dictBuffer.position(originalPosition); - } - - /** - * Helper method to move a PtNode to the tail of the file. - */ - private static int movePtNode(final OutputStream destination, - final Ver3DictUpdater dictUpdater, final PtNodeInfo info, - final int nodeArrayOriginAddress, final int oldNodeAddress, - final FormatOptions formatOptions) throws IOException { - final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); - updateParentAddress(dictUpdater, oldNodeAddress, dictBuffer.limit() + 1, formatOptions); - dictBuffer.position(oldNodeAddress); - final int currentFlags = dictBuffer.readUnsignedByte(); - dictBuffer.position(oldNodeAddress); - dictBuffer.put((byte)(FormatSpec.FLAG_IS_MOVED | (currentFlags - & (~FormatSpec.MASK_MOVE_AND_DELETE_FLAG)))); - int size = FormatSpec.PTNODE_FLAGS_SIZE; - updateForwardLink(dictUpdater, nodeArrayOriginAddress, dictBuffer.limit(), formatOptions); - size += BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { info }); - return size; - } - - @SuppressWarnings("unused") - private static void updateForwardLink(final Ver3DictUpdater dictUpdater, - final int nodeArrayOriginAddress, final int newNodeArrayAddress, - final FormatOptions formatOptions) { - final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); - dictUpdater.setPosition(nodeArrayOriginAddress); - int jumpCount = 0; - while (jumpCount++ < MAX_JUMPS) { - final int count = dictUpdater.readPtNodeCount(); - for (int i = 0; i < count; ++i) { - dictUpdater.readPtNode(dictUpdater.getPosition(), formatOptions); - } - final int forwardLinkAddress = dictBuffer.readUnsignedInt24(); - if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) { - dictBuffer.position(dictBuffer.position() - FormatSpec.FORWARD_LINK_ADDRESS_SIZE); - BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, newNodeArrayAddress); - return; - } - dictBuffer.position(forwardLinkAddress); - } - if (DBG && jumpCount >= MAX_JUMPS) { - throw new RuntimeException("too many jumps, probably a bug."); - } - } - - /** - * Move a PtNode that is referred to by oldPtNodeOrigin to the tail of the file, and set the - * children address to the byte after the PtNode. - * - * @param fileEndAddress the address of the tail of the file. - * @param codePoints the characters to put inside the PtNode. - * @param length how many code points to read from codePoints. - * @param flags the flags for this PtNode. - * @param frequency the frequency of this terminal. - * @param parentAddress the address of the parent PtNode of this PtNode. - * @param shortcutTargets the shortcut targets for this PtNode. - * @param bigrams the bigrams for this PtNode. - * @param destination the stream representing the tail of the file. - * @param dictUpdater the DictUpdater. - * @param oldPtNodeArrayOrigin the origin of the old PtNode array this PtNode was a part of. - * @param oldPtNodeOrigin the old origin where this PtNode used to be stored. - * @param formatOptions format options for this dictionary. - * @return the size written, in bytes. - * @throws IOException if the file can't be accessed - */ - private static int movePtNode(final int fileEndAddress, final int[] codePoints, - final int length, final int flags, final int frequency, final int parentAddress, - final ArrayList<WeightedString> shortcutTargets, - final ArrayList<PendingAttribute> bigrams, final OutputStream destination, - final Ver3DictUpdater dictUpdater, final int oldPtNodeArrayOrigin, - final int oldPtNodeOrigin, final FormatOptions formatOptions) throws IOException { - int size = 0; - final int newPtNodeOrigin = fileEndAddress + 1; - final int[] writtenCharacters = Arrays.copyOfRange(codePoints, 0, length); - final PtNodeInfo tmpInfo = new PtNodeInfo(newPtNodeOrigin, -1 /* endAddress */, - flags, writtenCharacters, frequency, parentAddress, FormatSpec.NO_CHILDREN_ADDRESS, - shortcutTargets, bigrams); - size = BinaryDictIOUtils.computePtNodeSize(tmpInfo, formatOptions); - final PtNodeInfo newInfo = new PtNodeInfo(newPtNodeOrigin, newPtNodeOrigin + size, - flags, writtenCharacters, frequency, parentAddress, - fileEndAddress + 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE, shortcutTargets, - bigrams); - movePtNode(destination, dictUpdater, newInfo, oldPtNodeArrayOrigin, oldPtNodeOrigin, - formatOptions); - return 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE; - } - - /** - * Converts a list of WeightedString to a list of PendingAttribute. - */ - public static ArrayList<PendingAttribute> resolveBigramPositions(final DictUpdater dictUpdater, - final ArrayList<WeightedString> bigramStrings) - throws IOException, UnsupportedFormatException { - if (bigramStrings == null) return CollectionUtils.newArrayList(); - final ArrayList<PendingAttribute> bigrams = CollectionUtils.newArrayList(); - for (final WeightedString bigram : bigramStrings) { - final int pos = dictUpdater.getTerminalPosition(bigram.mWord); - if (pos == FormatSpec.NOT_VALID_WORD) { - // TODO: figure out what is the correct thing to do here. - } else { - bigrams.add(new PendingAttribute(bigram.mFrequency, pos)); - } - } - return bigrams; - } - - /** - * Insert a word into a binary dictionary. - * - * @param dictUpdater the dict updater. - * @param destination a stream to the underlying file, with the pointer at the end of the file. - * @param word the word to insert. - * @param frequency the frequency of the new word. - * @param bigramStrings bigram list, or null if none. - * @param shortcuts shortcut list, or null if none. - * @param isBlackListEntry whether this should be a blacklist entry. - * @throws IOException if the file can't be accessed. - * @throws UnsupportedFormatException if the existing dictionary is in an unexpected format. - */ - // TODO: Support batch insertion. - // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary. - @UsedForTesting - public static void insertWord(final Ver3DictUpdater dictUpdater, - final OutputStream destination, final String word, final int frequency, - final ArrayList<WeightedString> bigramStrings, - final ArrayList<WeightedString> shortcuts, final boolean isNotAWord, - final boolean isBlackListEntry) - throws IOException, UnsupportedFormatException { - final ArrayList<PendingAttribute> bigrams = resolveBigramPositions(dictUpdater, - bigramStrings); - final DictBuffer dictBuffer = dictUpdater.getDictBuffer(); - - final boolean isTerminal = true; - final boolean hasBigrams = !bigrams.isEmpty(); - final boolean hasShortcuts = shortcuts != null && !shortcuts.isEmpty(); - - // find the insert position of the word. - if (dictBuffer.position() != 0) dictBuffer.position(0); - final FileHeader fileHeader = dictUpdater.readHeader(); - - int wordPos = 0, address = dictBuffer.position(), nodeOriginAddress = dictBuffer.position(); - final int[] codePoints = FusionDictionary.getCodePoints(word); - final int wordLen = codePoints.length; - - for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) { - if (wordPos >= wordLen) break; - nodeOriginAddress = dictBuffer.position(); - int nodeParentAddress = -1; - final int ptNodeCount = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer); - boolean foundNextNode = false; - - for (int i = 0; i < ptNodeCount; ++i) { - address = dictBuffer.position(); - final PtNodeInfo currentInfo = dictUpdater.readPtNode(address, - fileHeader.mFormatOptions); - final boolean isMovedNode = BinaryDictIOUtils.isMovedPtNode(currentInfo.mFlags, - fileHeader.mFormatOptions); - if (isMovedNode) continue; - nodeParentAddress = (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) - ? FormatSpec.NO_PARENT_ADDRESS : currentInfo.mParentAddress + address; - boolean matched = true; - for (int p = 0; p < currentInfo.mCharacters.length; ++p) { - if (wordPos + p >= wordLen) { - /* - * splitting - * before - * abcd - ef - * - * insert "abc" - * - * after - * abc - d - ef - */ - final int newNodeAddress = dictBuffer.limit(); - final int flags = BinaryDictEncoderUtils.makePtNodeFlags(p > 1, - isTerminal, 0, hasShortcuts, hasBigrams, false /* isNotAWord */, - false /* isBlackListEntry */, fileHeader.mFormatOptions); - int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p, flags, - frequency, nodeParentAddress, shortcuts, bigrams, destination, - dictUpdater, nodeOriginAddress, address, fileHeader.mFormatOptions); - - final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p, - currentInfo.mCharacters.length); - if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { - updateParentAddresses(dictUpdater, currentInfo.mChildrenAddress, - newNodeAddress + written + 1, fileHeader.mFormatOptions); - } - final PtNodeInfo newInfo2 = new PtNodeInfo( - newNodeAddress + written + 1, -1 /* endAddress */, - currentInfo.mFlags, characters2, currentInfo.mFrequency, - newNodeAddress + 1, currentInfo.mChildrenAddress, - currentInfo.mShortcutTargets, currentInfo.mBigrams); - BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { newInfo2 }); - return; - } else if (codePoints[wordPos + p] != currentInfo.mCharacters[p]) { - if (p > 0) { - /* - * splitting - * before - * ab - cd - * - * insert "ac" - * - * after - * a - b - cd - * | - * - c - */ - - final int newNodeAddress = dictBuffer.limit(); - final int childrenAddress = currentInfo.mChildrenAddress; - - // move prefix - final int prefixFlags = BinaryDictEncoderUtils.makePtNodeFlags(p > 1, - false /* isTerminal */, 0 /* childrenAddressSize*/, - false /* hasShortcut */, false /* hasBigrams */, - false /* isNotAWord */, false /* isBlackListEntry */, - fileHeader.mFormatOptions); - int written = movePtNode(newNodeAddress, currentInfo.mCharacters, p, - prefixFlags, -1 /* frequency */, nodeParentAddress, null, null, - destination, dictUpdater, nodeOriginAddress, address, - fileHeader.mFormatOptions); - - final int[] suffixCharacters = Arrays.copyOfRange( - currentInfo.mCharacters, p, currentInfo.mCharacters.length); - if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { - updateParentAddresses(dictUpdater, currentInfo.mChildrenAddress, - newNodeAddress + written + 1, fileHeader.mFormatOptions); - } - final int suffixFlags = BinaryDictEncoderUtils.makePtNodeFlags( - suffixCharacters.length > 1, - (currentInfo.mFlags & FormatSpec.FLAG_IS_TERMINAL) != 0, - 0 /* childrenAddressSize */, - (currentInfo.mFlags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS) - != 0, - (currentInfo.mFlags & FormatSpec.FLAG_HAS_BIGRAMS) != 0, - isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); - final PtNodeInfo suffixInfo = new PtNodeInfo( - newNodeAddress + written + 1, -1 /* endAddress */, suffixFlags, - suffixCharacters, currentInfo.mFrequency, newNodeAddress + 1, - currentInfo.mChildrenAddress, currentInfo.mShortcutTargets, - currentInfo.mBigrams); - written += BinaryDictIOUtils.computePtNodeSize(suffixInfo, - fileHeader.mFormatOptions) + 1; - - final int[] newCharacters = Arrays.copyOfRange(codePoints, wordPos + p, - codePoints.length); - final int flags = BinaryDictEncoderUtils.makePtNodeFlags( - newCharacters.length > 1, isTerminal, - 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); - final PtNodeInfo newInfo = new PtNodeInfo( - newNodeAddress + written, -1 /* endAddress */, flags, - newCharacters, frequency, newNodeAddress + 1, - FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams); - BinaryDictIOUtils.writeNodes(destination, - new PtNodeInfo[] { suffixInfo, newInfo }); - return; - } - matched = false; - break; - } - } - - if (matched) { - if (wordPos + currentInfo.mCharacters.length == wordLen) { - // the word exists in the dictionary. - // only update the PtNode. - final int newNodeAddress = dictBuffer.limit(); - final boolean hasMultipleChars = currentInfo.mCharacters.length > 1; - final int flags = BinaryDictEncoderUtils.makePtNodeFlags(hasMultipleChars, - isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); - final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress + 1, - -1 /* endAddress */, flags, currentInfo.mCharacters, frequency, - nodeParentAddress, currentInfo.mChildrenAddress, shortcuts, - bigrams); - movePtNode(destination, dictUpdater, newInfo, nodeOriginAddress, address, - fileHeader.mFormatOptions); - return; - } - wordPos += currentInfo.mCharacters.length; - if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) { - /* - * found the prefix of the word. - * make new PtNode and link to the PtNode from this PtNode. - * - * before - * ab - cd - * - * insert "abcde" - * - * after - * ab - cd - e - */ - final int newNodeArrayAddress = dictBuffer.limit(); - updateChildrenAddress(dictUpdater, address, newNodeArrayAddress, - fileHeader.mFormatOptions); - final int newNodeAddress = newNodeArrayAddress + 1; - final boolean hasMultipleChars = (wordLen - wordPos) > 1; - final int flags = BinaryDictEncoderUtils.makePtNodeFlags(hasMultipleChars, - isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); - final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen); - final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress, -1, flags, - characters, frequency, address, FormatSpec.NO_CHILDREN_ADDRESS, - shortcuts, bigrams); - BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { newInfo }); - return; - } - dictBuffer.position(currentInfo.mChildrenAddress); - foundNextNode = true; - break; - } - } - - if (foundNextNode) continue; - - // reached the end of the array. - final int linkAddressPosition = dictBuffer.position(); - int nextLink = dictBuffer.readUnsignedInt24(); - if ((nextLink & FormatSpec.MSB24) != 0) { - nextLink = -(nextLink & FormatSpec.SINT24_MAX); - } - if (nextLink == FormatSpec.NO_FORWARD_LINK_ADDRESS) { - /* - * expand this node. - * - * before - * ab - cd - * - * insert "abef" - * - * after - * ab - cd - * | - * - ef - */ - - // change the forward link address. - final int newNodeAddress = dictBuffer.limit(); - dictBuffer.position(linkAddressPosition); - BinaryDictIOUtils.writeSInt24ToBuffer(dictBuffer, newNodeAddress); - - final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen); - final int flags = BinaryDictEncoderUtils.makePtNodeFlags(characters.length > 1, - isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams, - isNotAWord, isBlackListEntry, fileHeader.mFormatOptions); - final PtNodeInfo newInfo = new PtNodeInfo(newNodeAddress + 1, - -1 /* endAddress */, flags, characters, frequency, nodeParentAddress, - FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams); - BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[]{ newInfo }); - return; - } else { - depth--; - dictBuffer.position(nextLink); - } - } - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java index b56234f6d..f25503488 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java +++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java @@ -18,10 +18,9 @@ package com.android.inputmethod.latin.makedict; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.makedict.DictDecoder.DictionaryBufferFactory; -import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; -import java.io.File; +import java.util.Date; +import java.util.HashMap; /** * Dictionary File Format Specification. @@ -40,12 +39,8 @@ public final class FormatSpec { * 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 - * o | has bigrams ? 1 bit, 1 = yes, 0 = no : CONTAINS_BIGRAMS_FLAG - * n | FRENCH_LIGATURE_PROCESSING_FLAG - * f | supports dynamic updates ? 1 bit, 1 = yes, 0 = no : SUPPORTS_DYNAMIC_UPDATE - * l | GERMAN_UMLAUT_PROCESSING_FLAG - * a | - * gs + * o | + * nflags * * h | * e | size of the file header, 4bytes @@ -82,45 +77,36 @@ public final class FormatSpec { * s * * f | - * o | IF SUPPORTS_DYNAMIC_UPDATE (defined in the file header) - * r | forward link address, 3byte - * w | 1 byte = bbbbbbbb match - * a | case 1xxxxxxx => -((xxxxxxx << 16) + (next byte << 8) + next byte) - * r | otherwise => (xxxxxxx << 16) + (next byte << 8) + next byte - * d | - * linkaddress + * o | forward link address, 3byte + * r | 1 byte = bbbbbbbb match + * w | case 1xxxxxxx => -((xxxxxxx << 16) + (next byte << 8) + next byte) + * a | otherwise => (xxxxxxx << 16) + (next byte << 8) + next byte + * r | + * dlinkaddress */ /* Node (FusionDictionary.PtNode) layout is as follows: - * | IF !SUPPORTS_DYNAMIC_UPDATE - * | addressType xx : mask with MASK_CHILDREN_ADDRESS_TYPE - * | 2 bits, 00 = no children : FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS - * f | 01 = 1 byte : FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE - * l | 10 = 2 bytes : FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES - * a | 11 = 3 bytes : FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES - * g | ELSE - * s | 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 - * | the new address is stored in the same place as the parent address - * | is deleted? 10 = yes : FLAG_IS_DELETED - * | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS - * | has a terminal ? 1 bit, 1 = yes, 0 = no : FLAG_IS_TERMINAL - * | has shortcut targets ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_SHORTCUT_TARGETS + * | 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 + * 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 * | has bigrams ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_BIGRAMS * | 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 | IF SUPPORTS_DYNAMIC_UPDATE (defined in the file header) - * r | parent address, 3byte - * e | 1 byte = bbbbbbbb match - * n | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte) - * t | otherwise => (bbbbbbbb << 16) + (next byte << 8) + next byte - * a | This address is relative to the head of the PtNode. - * d | If the node doesn't have a parent, this field is set to 0. + * 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 | - * ress + * dress * * c | IF FLAG_HAS_MULTIPLE_CHARS * h | char, char, char, char n * (1 or 3 bytes) : use PtNodeInfo for i/o helpers @@ -134,23 +120,16 @@ public final class FormatSpec { * e | frequency 1 byte * q | * - * c | IF SUPPORTS_DYNAMIC_UPDATE - * 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 | ELSIF 00 = FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS == addressType - * a | // nothing - * d | ELSIF 01 = FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE == addressType - * d | children address, 1 byte - * r | ELSIF 10 = FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES == addressType - * e | children address, 2 bytes - * s | ELSE // 11 = FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES = addressType - * s | children address, 3 bytes - * | END - * | This address is relative to the position of this field. + * 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 * * | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS * | shortcut string list @@ -199,21 +178,18 @@ public final class FormatSpec { */ public static final int MAGIC_NUMBER = 0x9BC13AFE; - static final int MINIMUM_SUPPORTED_VERSION = 2; - static final int MAXIMUM_SUPPORTED_VERSION = 4; 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; - static final int VERSION3 = 3; - static final int VERSION4 = 4; - // These options need to be the same numeric values as the one in the native reading code. - static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1; - // TODO: Make the native reading code read this variable. - static final int SUPPORTS_DYNAMIC_UPDATE = 0x2; - static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4; - static final int CONTAINS_BIGRAMS_FLAG = 0x8; - static final int CONTAINS_TIMESTAMP_FLAG = 0x10; + // 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 + // us to change the format during development while having testing devices remove + // older files with each upgrade, while still having a readable versioning scheme. + public static final int VERSION2 = 2; + public static final int VERSION4 = 401; + static final int MINIMUM_SUPPORTED_VERSION = VERSION2; + static final int MAXIMUM_SUPPORTED_VERSION = VERSION4; // TODO: Make this value adaptative to content data, store it in the header, and // use it in the reading code. @@ -263,29 +239,31 @@ public final class FormatSpec { 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. + // 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"; - static final String UNIGRAM_TIMESTAMP_FILE_EXTENSION = ".timestamp"; // 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 584KB with the block size being 4. - // This is 91% of that of full address table. - static final int BIGRAM_ADDRESS_TABLE_BLOCK_SIZE = 4; - static final int BIGRAM_CONTENT_COUNT = 2; + // 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 int BIGRAM_TIMESTAMP_CONTENT_INDEX = 1; static final String BIGRAM_FREQ_CONTENT_ID = "_freq"; - static final String BIGRAM_TIMESTAMP_CONTENT_ID = "_timestamp"; static final int BIGRAM_TIMESTAMP_SIZE = 4; static final int BIGRAM_COUNTER_SIZE = 1; static final int BIGRAM_LEVEL_SIZE = 1; @@ -293,7 +271,7 @@ public final class FormatSpec { 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 - // 29KB with the block size being 64. + // 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"; @@ -331,107 +309,56 @@ public final class FormatSpec { */ public static final class FormatOptions { public final int mVersion; - public final boolean mSupportsDynamicUpdate; - public final boolean mHasTerminalId; public final boolean mHasTimestamp; - @UsedForTesting - public FormatOptions(final int version) { - this(version, false); - } @UsedForTesting - public FormatOptions(final int version, final boolean supportsDynamicUpdate) { - this(version, supportsDynamicUpdate, false /* hasTimestamp */); + public FormatOptions(final int version) { + this(version, false /* hasTimestamp */); } - public FormatOptions(final int version, final boolean supportsDynamicUpdate, - final boolean hasTimestamp) { + public FormatOptions(final int version, final boolean hasTimestamp) { mVersion = version; - if (version < FIRST_VERSION_WITH_DYNAMIC_UPDATE && supportsDynamicUpdate) { - throw new RuntimeException("Dynamic updates are only supported with versions " - + FIRST_VERSION_WITH_DYNAMIC_UPDATE + " and ulterior."); - } - mSupportsDynamicUpdate = supportsDynamicUpdate; - mHasTerminalId = (version >= FIRST_VERSION_WITH_TERMINAL_ID); mHasTimestamp = hasTimestamp; } } /** - * Class representing file header. + * Options global to the dictionary. */ - public static final class FileHeader { - public final int mHeaderSize; - public final DictionaryOptions mDictionaryOptions; - public final FormatOptions mFormatOptions; - // Note that these are corresponding definitions in native code in latinime::HeaderPolicy - // and latinime::HeaderReadWriteUtils. - public static final String SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE = "SUPPORTS_DYNAMIC_UPDATE"; - public static final String USES_FORGETTING_CURVE_ATTRIBUTE = "USES_FORGETTING_CURVE"; - public static final String ATTRIBUTE_VALUE_TRUE = "1"; - - public static final String DICTIONARY_VERSION_ATTRIBUTE = "version"; - public static final String DICTIONARY_LOCALE_ATTRIBUTE = "locale"; - public static final String DICTIONARY_ID_ATTRIBUTE = "dictionary"; - private static final String DICTIONARY_DESCRIPTION_ATTRIBUTE = "description"; - public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions, - final FormatOptions formatOptions) { - mHeaderSize = headerSize; - mDictionaryOptions = dictionaryOptions; - mFormatOptions = formatOptions; - } - - // Helper method to get the locale as a String - public String getLocaleString() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_LOCALE_ATTRIBUTE); - } - - // Helper method to get the version String - public String getVersion() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_VERSION_ATTRIBUTE); + public static final class DictionaryOptions { + public final HashMap<String, String> mAttributes; + public DictionaryOptions(final HashMap<String, String> attributes) { + mAttributes = attributes; } - - // Helper method to get the dictionary ID as a String - public String getId() { - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_ID_ATTRIBUTE); - } - - // Helper method to get the description - public String getDescription() { - // TODO: Right now each dictionary file comes with a description in its own language. - // It will display as is no matter the device's locale. It should be internationalized. - return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_DESCRIPTION_ATTRIBUTE); + @Override + public String toString() { // Convenience method + return toString(0, false); } - } - - /** - * Returns new dictionary decoder. - * - * @param dictFile the dictionary file. - * @param bufferType The type of buffer, as one of USE_* in DictDecoder. - * @return new dictionary decoder if the dictionary file exists, otherwise null. - */ - public static DictDecoder getDictDecoder(final File dictFile, final int bufferType) { - if (dictFile.isDirectory()) { - return new Ver4DictDecoder(dictFile, bufferType); - } else if (dictFile.isFile()) { - return new Ver3DictDecoder(dictFile, bufferType); - } - return null; - } - - public static DictDecoder getDictDecoder(final File dictFile, - final DictionaryBufferFactory factory) { - if (dictFile.isDirectory()) { - return new Ver4DictDecoder(dictFile, factory); - } else if (dictFile.isFile()) { - return new Ver3DictDecoder(dictFile, factory); + public String toString(final int indentCount, final boolean plumbing) { + final StringBuilder indent = new StringBuilder(); + if (plumbing) { + indent.append("H:"); + } else { + for (int i = 0; i < indentCount; ++i) { + indent.append(" "); + } + } + final StringBuilder s = new StringBuilder(); + for (final String optionKey : mAttributes.keySet()) { + s.append(indent); + s.append(optionKey); + s.append(" = "); + if ("date".equals(optionKey) && !plumbing) { + // Date needs a number of milliseconds, but the dictionary contains seconds + s.append(new Date( + 1000 * Long.parseLong(mAttributes.get(optionKey))).toString()); + } else { + s.append(mAttributes.get(optionKey)); + } + s.append("\n"); + } + return s.toString(); } - return null; - } - - public static DictDecoder getDictDecoder(final File dictFile) { - return getDictDecoder(dictFile, DictDecoder.USE_READONLY_BYTEBUFFER); } private FormatSpec() { diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java deleted file mode 100644 index 3bb218bea..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java +++ /dev/null @@ -1,916 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Constants; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; - -/** - * A dictionary that can fusion heads and tails of words for more compression. - */ -@UsedForTesting -public final class FusionDictionary implements Iterable<Word> { - private static final boolean DBG = MakedictLog.DBG; - - private static int CHARACTER_NOT_FOUND_INDEX = -1; - - /** - * A node array of the dictionary, containing several PtNodes. - * - * A PtNodeArray is but an ordered array of PtNodes, which essentially contain all the - * real information. - * This class also contains fields to cache size and address, to help with binary - * generation. - */ - public static final class PtNodeArray { - ArrayList<PtNode> mData; - // To help with binary generation - int mCachedSize = Integer.MIN_VALUE; - // mCachedAddressBefore/AfterUpdate are helpers for binary dictionary generation. They - // always hold the same value except between dictionary address compression, during which - // the update process needs to know about both values at the same time. Updating will - // update the AfterUpdate value, and the code will move them to BeforeUpdate before - // the next update pass. - int mCachedAddressBeforeUpdate = Integer.MIN_VALUE; - int mCachedAddressAfterUpdate = Integer.MIN_VALUE; - int mCachedParentAddress = 0; - - public PtNodeArray() { - mData = new ArrayList<PtNode>(); - } - public PtNodeArray(ArrayList<PtNode> data) { - mData = data; - } - } - - /** - * A string with a frequency. - * - * This represents an "attribute", that is either a bigram or a shortcut. - */ - public static final class WeightedString { - public final String mWord; - public int mFrequency; - public WeightedString(String word, int frequency) { - mWord = word; - mFrequency = frequency; - } - - @Override - public int hashCode() { - return Arrays.hashCode(new Object[] { mWord, mFrequency }); - } - - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (!(o instanceof WeightedString)) return false; - WeightedString w = (WeightedString)o; - return mWord.equals(w.mWord) && mFrequency == w.mFrequency; - } - } - - /** - * PtNode is a group of characters, with a frequency, shortcut targets, bigrams, and children - * (Pt means Patricia Trie). - * - * This is the central class of the in-memory representation. A PtNode is what can - * be seen as a traditional "trie node", except it can hold several characters at the - * same time. A PtNode essentially represents one or several characters in the middle - * of the trie tree; as such, it can be a terminal, and it can have children. - * In this in-memory representation, whether the PtNode is a terminal or not is represented - * in the frequency, where NOT_A_TERMINAL (= -1) means this is not a terminal and any other - * value is the frequency of this terminal. A terminal may have non-null shortcuts and/or - * bigrams, but a non-terminal may not. Moreover, children, if present, are null. - */ - public static final class PtNode { - public static final int NOT_A_TERMINAL = -1; - final int mChars[]; - ArrayList<WeightedString> mShortcutTargets; - ArrayList<WeightedString> mBigrams; - int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal. - int mTerminalId; // NOT_A_TERMINAL == mTerminalId indicates this is not a terminal. - PtNodeArray mChildren; - boolean mIsNotAWord; // Only a shortcut - boolean mIsBlacklistEntry; - // mCachedSize and mCachedAddressBefore/AfterUpdate are helpers for binary dictionary - // generation. Before and After always hold the same value except during dictionary - // address compression, where the update process needs to know about both values at the - // same time. Updating will update the AfterUpdate value, and the code will move them - // to BeforeUpdate before the next update pass. - // The update process does not need two versions of mCachedSize. - int mCachedSize; // The size, in bytes, of this PtNode. - int mCachedAddressBeforeUpdate; // The address of this PtNode (before update) - int mCachedAddressAfterUpdate; // The address of this PtNode (after update) - - public PtNode(final int[] chars, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams, final int frequency, - final boolean isNotAWord, final boolean isBlacklistEntry) { - mChars = chars; - mFrequency = frequency; - mTerminalId = frequency; - mShortcutTargets = shortcutTargets; - mBigrams = bigrams; - mChildren = null; - mIsNotAWord = isNotAWord; - mIsBlacklistEntry = isBlacklistEntry; - } - - public PtNode(final int[] chars, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams, final int frequency, - final boolean isNotAWord, final boolean isBlacklistEntry, - final PtNodeArray children) { - mChars = chars; - mFrequency = frequency; - mShortcutTargets = shortcutTargets; - mBigrams = bigrams; - mChildren = children; - mIsNotAWord = isNotAWord; - mIsBlacklistEntry = isBlacklistEntry; - } - - public void addChild(PtNode n) { - if (null == mChildren) { - mChildren = new PtNodeArray(); - } - mChildren.mData.add(n); - } - - public int getTerminalId() { - return mTerminalId; - } - - public boolean isTerminal() { - return NOT_A_TERMINAL != mFrequency; - } - - public int getFrequency() { - return mFrequency; - } - - public boolean getIsNotAWord() { - return mIsNotAWord; - } - - public boolean getIsBlacklistEntry() { - return mIsBlacklistEntry; - } - - public ArrayList<WeightedString> getShortcutTargets() { - // We don't want write permission to escape outside the package, so we return a copy - if (null == mShortcutTargets) return null; - final ArrayList<WeightedString> copyOfShortcutTargets = - new ArrayList<WeightedString>(mShortcutTargets); - return copyOfShortcutTargets; - } - - public ArrayList<WeightedString> getBigrams() { - // We don't want write permission to escape outside the package, so we return a copy - if (null == mBigrams) return null; - final ArrayList<WeightedString> copyOfBigrams = new ArrayList<WeightedString>(mBigrams); - return copyOfBigrams; - } - - public boolean hasSeveralChars() { - assert(mChars.length > 0); - return 1 < mChars.length; - } - - /** - * Adds a word to the bigram list. Updates the frequency if the word already - * exists. - */ - public void addBigram(final String word, final int frequency) { - if (mBigrams == null) { - mBigrams = new ArrayList<WeightedString>(); - } - WeightedString bigram = getBigram(word); - if (bigram != null) { - bigram.mFrequency = frequency; - } else { - bigram = new WeightedString(word, frequency); - mBigrams.add(bigram); - } - } - - /** - * Gets the shortcut target for the given word. Returns null if the word is not in the - * shortcut list. - */ - public WeightedString getShortcut(final String word) { - // TODO: Don't do a linear search - if (mShortcutTargets != null) { - final int size = mShortcutTargets.size(); - for (int i = 0; i < size; ++i) { - WeightedString shortcut = mShortcutTargets.get(i); - if (shortcut.mWord.equals(word)) { - return shortcut; - } - } - } - return null; - } - - /** - * Gets the bigram for the given word. - * Returns null if the word is not in the bigrams list. - */ - public WeightedString getBigram(final String word) { - // TODO: Don't do a linear search - if (mBigrams != null) { - final int size = mBigrams.size(); - for (int i = 0; i < size; ++i) { - WeightedString bigram = mBigrams.get(i); - if (bigram.mWord.equals(word)) { - return bigram; - } - } - } - return null; - } - - /** - * Updates the PtNode with the given properties. Adds the shortcut and bigram lists to - * the existing ones if any. Note: unigram, bigram, and shortcut frequencies are only - * updated if they are higher than the existing ones. - */ - public void update(final int frequency, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams, - final boolean isNotAWord, final boolean isBlacklistEntry) { - if (frequency > mFrequency) { - mFrequency = frequency; - } - if (shortcutTargets != null) { - if (mShortcutTargets == null) { - mShortcutTargets = shortcutTargets; - } else { - final int size = shortcutTargets.size(); - for (int i = 0; i < size; ++i) { - final WeightedString shortcut = shortcutTargets.get(i); - final WeightedString existingShortcut = getShortcut(shortcut.mWord); - if (existingShortcut == null) { - mShortcutTargets.add(shortcut); - } else if (existingShortcut.mFrequency < shortcut.mFrequency) { - existingShortcut.mFrequency = shortcut.mFrequency; - } - } - } - } - if (bigrams != null) { - if (mBigrams == null) { - mBigrams = bigrams; - } else { - final int size = bigrams.size(); - for (int i = 0; i < size; ++i) { - final WeightedString bigram = bigrams.get(i); - final WeightedString existingBigram = getBigram(bigram.mWord); - if (existingBigram == null) { - mBigrams.add(bigram); - } else if (existingBigram.mFrequency < bigram.mFrequency) { - existingBigram.mFrequency = bigram.mFrequency; - } - } - } - } - mIsNotAWord = isNotAWord; - mIsBlacklistEntry = isBlacklistEntry; - } - } - - /** - * Options global to the dictionary. - */ - public static final class DictionaryOptions { - public final boolean mGermanUmlautProcessing; - public final boolean mFrenchLigatureProcessing; - public final HashMap<String, String> mAttributes; - public DictionaryOptions(final HashMap<String, String> attributes, - final boolean germanUmlautProcessing, final boolean frenchLigatureProcessing) { - mAttributes = attributes; - mGermanUmlautProcessing = germanUmlautProcessing; - mFrenchLigatureProcessing = frenchLigatureProcessing; - } - @Override - public String toString() { // Convenience method - return toString(0, false); - } - public String toString(final int indentCount, final boolean plumbing) { - final StringBuilder indent = new StringBuilder(); - if (plumbing) { - indent.append("H:"); - } else { - for (int i = 0; i < indentCount; ++i) { - indent.append(" "); - } - } - final StringBuilder s = new StringBuilder(); - for (final String optionKey : mAttributes.keySet()) { - s.append(indent); - s.append(optionKey); - s.append(" = "); - if ("date".equals(optionKey) && !plumbing) { - // Date needs a number of milliseconds, but the dictionary contains seconds - s.append(new Date( - 1000 * Long.parseLong(mAttributes.get(optionKey))).toString()); - } else { - s.append(mAttributes.get(optionKey)); - } - s.append("\n"); - } - if (mGermanUmlautProcessing) { - s.append(indent); - s.append("Needs German umlaut processing\n"); - } - if (mFrenchLigatureProcessing) { - s.append(indent); - s.append("Needs French ligature processing\n"); - } - return s.toString(); - } - } - - public final DictionaryOptions mOptions; - public final PtNodeArray mRootNodeArray; - - public FusionDictionary(final PtNodeArray rootNodeArray, final DictionaryOptions options) { - mRootNodeArray = rootNodeArray; - mOptions = options; - } - - public void addOptionAttribute(final String key, final String value) { - mOptions.mAttributes.put(key, value); - } - - /** - * Helper method to convert a String to an int array. - */ - static int[] getCodePoints(final String word) { - // TODO: this is a copy-paste of the old contents of StringUtils.toCodePointArray, - // which is not visible from the makedict package. Factor this code. - final int length = word.length(); - if (length <= 0) return new int[] {}; - final char[] characters = word.toCharArray(); - final int[] codePoints = new int[Character.codePointCount(characters, 0, length)]; - int codePoint = Character.codePointAt(characters, 0); - int dsti = 0; - for (int srci = Character.charCount(codePoint); - srci < length; srci += Character.charCount(codePoint), ++dsti) { - codePoints[dsti] = codePoint; - codePoint = Character.codePointAt(characters, srci); - } - codePoints[dsti] = codePoint; - return codePoints; - } - - /** - * Helper method to add a word as a string. - * - * This method adds a word to the dictionary with the given frequency. Optional - * lists of bigrams and shortcuts can be passed here. For each word inside, - * they will be added to the dictionary as necessary. - * - * @param word the word to add. - * @param frequency the frequency of the word, in the range [0..255]. - * @param shortcutTargets a list of shortcut targets for this word, or null. - * @param isNotAWord true if this should not be considered a word (e.g. shortcut only) - */ - public void add(final String word, final int frequency, - final ArrayList<WeightedString> shortcutTargets, final boolean isNotAWord) { - add(getCodePoints(word), frequency, shortcutTargets, isNotAWord, - false /* isBlacklistEntry */); - } - - /** - * Helper method to add a blacklist entry as a string. - * - * @param word the word to add as a blacklist entry. - * @param shortcutTargets a list of shortcut targets for this word, or null. - * @param isNotAWord true if this is not a word for spellcheking purposes (shortcut only or so) - */ - public void addBlacklistEntry(final String word, - final ArrayList<WeightedString> shortcutTargets, final boolean isNotAWord) { - add(getCodePoints(word), 0, shortcutTargets, isNotAWord, true /* isBlacklistEntry */); - } - - /** - * Sanity check for a PtNode array. - * - * This method checks that all PtNodes in a node array are ordered as expected. - * If they are, nothing happens. If they aren't, an exception is thrown. - */ - private void checkStack(PtNodeArray ptNodeArray) { - ArrayList<PtNode> stack = ptNodeArray.mData; - int lastValue = -1; - for (int i = 0; i < stack.size(); ++i) { - int currentValue = stack.get(i).mChars[0]; - if (currentValue <= lastValue) - throw new RuntimeException("Invalid stack"); - else - lastValue = currentValue; - } - } - - /** - * Helper method to add a new bigram to the dictionary. - * - * @param word1 the previous word of the context - * @param word2 the next word of the context - * @param frequency the bigram frequency - */ - public void setBigram(final String word1, final String word2, final int frequency) { - PtNode ptNode = findWordInTree(mRootNodeArray, word1); - if (ptNode != null) { - final PtNode ptNode2 = findWordInTree(mRootNodeArray, word2); - if (ptNode2 == null) { - add(getCodePoints(word2), 0, null, false /* isNotAWord */, - false /* isBlacklistEntry */); - // The PtNode for the first word may have moved by the above insertion, - // if word1 and word2 share a common stem that happens not to have been - // a cutting point until now. In this case, we need to refresh ptNode. - ptNode = findWordInTree(mRootNodeArray, word1); - } - ptNode.addBigram(word2, frequency); - } else { - throw new RuntimeException("First word of bigram not found"); - } - } - - /** - * Add a word to this dictionary. - * - * The shortcuts, if any, have to be in the dictionary already. If they aren't, - * an exception is thrown. - * - * @param word the word, as an int array. - * @param frequency the frequency of the word, in the range [0..255]. - * @param shortcutTargets an optional list of shortcut targets for this word (null if none). - * @param isNotAWord true if this is not a word for spellcheking purposes (shortcut only or so) - * @param isBlacklistEntry true if this is a blacklisted word, false otherwise - */ - private void add(final int[] word, final int frequency, - final ArrayList<WeightedString> shortcutTargets, - final boolean isNotAWord, final boolean isBlacklistEntry) { - assert(frequency >= 0 && frequency <= 255); - if (word.length >= Constants.DICTIONARY_MAX_WORD_LENGTH) { - MakedictLog.w("Ignoring a word that is too long: word.length = " + word.length); - return; - } - - PtNodeArray currentNodeArray = mRootNodeArray; - int charIndex = 0; - - PtNode currentPtNode = null; - int differentCharIndex = 0; // Set by the loop to the index of the char that differs - int nodeIndex = findIndexOfChar(mRootNodeArray, word[charIndex]); - while (CHARACTER_NOT_FOUND_INDEX != nodeIndex) { - currentPtNode = currentNodeArray.mData.get(nodeIndex); - differentCharIndex = compareCharArrays(currentPtNode.mChars, word, charIndex); - if (ARRAYS_ARE_EQUAL != differentCharIndex - && differentCharIndex < currentPtNode.mChars.length) break; - if (null == currentPtNode.mChildren) break; - charIndex += currentPtNode.mChars.length; - if (charIndex >= word.length) break; - currentNodeArray = currentPtNode.mChildren; - nodeIndex = findIndexOfChar(currentNodeArray, word[charIndex]); - } - - if (CHARACTER_NOT_FOUND_INDEX == nodeIndex) { - // No node at this point to accept the word. Create one. - final int insertionIndex = findInsertionIndex(currentNodeArray, word[charIndex]); - final PtNode newPtNode = new PtNode(Arrays.copyOfRange(word, charIndex, word.length), - shortcutTargets, null /* bigrams */, frequency, isNotAWord, isBlacklistEntry); - currentNodeArray.mData.add(insertionIndex, newPtNode); - if (DBG) checkStack(currentNodeArray); - } else { - // There is a word with a common prefix. - if (differentCharIndex == currentPtNode.mChars.length) { - if (charIndex + differentCharIndex >= word.length) { - // The new word is a prefix of an existing word, but the node on which it - // should end already exists as is. Since the old PtNode was not a terminal, - // make it one by filling in its frequency and other attributes - currentPtNode.update(frequency, shortcutTargets, null, isNotAWord, - isBlacklistEntry); - } else { - // The new word matches the full old word and extends past it. - // We only have to create a new node and add it to the end of this. - final PtNode newNode = new PtNode( - Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length), - shortcutTargets, null /* bigrams */, frequency, isNotAWord, - isBlacklistEntry); - currentPtNode.mChildren = new PtNodeArray(); - currentPtNode.mChildren.mData.add(newNode); - } - } else { - if (0 == differentCharIndex) { - // Exact same word. Update the frequency if higher. This will also add the - // new shortcuts to the existing shortcut list if it already exists. - currentPtNode.update(frequency, shortcutTargets, null, - currentPtNode.mIsNotAWord && isNotAWord, - currentPtNode.mIsBlacklistEntry || isBlacklistEntry); - } else { - // Partial prefix match only. We have to replace the current node with a node - // containing the current prefix and create two new ones for the tails. - PtNodeArray newChildren = new PtNodeArray(); - final PtNode newOldWord = new PtNode( - Arrays.copyOfRange(currentPtNode.mChars, differentCharIndex, - currentPtNode.mChars.length), currentPtNode.mShortcutTargets, - currentPtNode.mBigrams, currentPtNode.mFrequency, - currentPtNode.mIsNotAWord, currentPtNode.mIsBlacklistEntry, - currentPtNode.mChildren); - newChildren.mData.add(newOldWord); - - final PtNode newParent; - if (charIndex + differentCharIndex >= word.length) { - newParent = new PtNode( - Arrays.copyOfRange(currentPtNode.mChars, 0, differentCharIndex), - shortcutTargets, null /* bigrams */, frequency, - isNotAWord, isBlacklistEntry, newChildren); - } else { - newParent = new PtNode( - Arrays.copyOfRange(currentPtNode.mChars, 0, differentCharIndex), - null /* shortcutTargets */, null /* bigrams */, -1, - false /* isNotAWord */, false /* isBlacklistEntry */, newChildren); - final PtNode newWord = new PtNode(Arrays.copyOfRange(word, - charIndex + differentCharIndex, word.length), - shortcutTargets, null /* bigrams */, frequency, - isNotAWord, isBlacklistEntry); - final int addIndex = word[charIndex + differentCharIndex] - > currentPtNode.mChars[differentCharIndex] ? 1 : 0; - newChildren.mData.add(addIndex, newWord); - } - currentNodeArray.mData.set(nodeIndex, newParent); - } - if (DBG) checkStack(currentNodeArray); - } - } - } - - private static int ARRAYS_ARE_EQUAL = 0; - - /** - * Custom comparison of two int arrays taken to contain character codes. - * - * This method compares the two arrays passed as an argument in a lexicographic way, - * with an offset in the dst string. - * This method does NOT test for the first character. It is taken to be equal. - * I repeat: this method starts the comparison at 1 <> dstOffset + 1. - * The index where the strings differ is returned. ARRAYS_ARE_EQUAL = 0 is returned if the - * strings are equal. This works BECAUSE we don't look at the first character. - * - * @param src the left-hand side string of the comparison. - * @param dst the right-hand side string of the comparison. - * @param dstOffset the offset in the right-hand side string. - * @return the index at which the strings differ, or ARRAYS_ARE_EQUAL = 0 if they don't. - */ - private static int compareCharArrays(final int[] src, final int[] dst, int dstOffset) { - // We do NOT test the first char, because we come from a method that already - // tested it. - for (int i = 1; i < src.length; ++i) { - if (dstOffset + i >= dst.length) return i; - if (src[i] != dst[dstOffset + i]) return i; - } - if (dst.length > src.length) return src.length; - return ARRAYS_ARE_EQUAL; - } - - /** - * Helper class that compares and sorts two PtNodes according to their - * first element only. I repeat: ONLY the first element is considered, the rest - * is ignored. - * This comparator imposes orderings that are inconsistent with equals. - */ - static private final class PtNodeComparator implements java.util.Comparator<PtNode> { - @Override - public int compare(PtNode p1, PtNode p2) { - if (p1.mChars[0] == p2.mChars[0]) return 0; - return p1.mChars[0] < p2.mChars[0] ? -1 : 1; - } - } - final static private PtNodeComparator PTNODE_COMPARATOR = new PtNodeComparator(); - - /** - * Finds the insertion index of a character within a node array. - */ - private static int findInsertionIndex(final PtNodeArray nodeArray, int character) { - final ArrayList<PtNode> data = nodeArray.mData; - final PtNode reference = new PtNode(new int[] { character }, - null /* shortcutTargets */, null /* bigrams */, 0, false /* isNotAWord */, - false /* isBlacklistEntry */); - int result = Collections.binarySearch(data, reference, PTNODE_COMPARATOR); - return result >= 0 ? result : -result - 1; - } - - /** - * Find the index of a char in a node array, if it exists. - * - * @param nodeArray the node array to search in. - * @param character the character to search for. - * @return the position of the character if it's there, or CHARACTER_NOT_FOUND_INDEX = -1 else. - */ - private static int findIndexOfChar(final PtNodeArray nodeArray, int character) { - final int insertionIndex = findInsertionIndex(nodeArray, character); - if (nodeArray.mData.size() <= insertionIndex) return CHARACTER_NOT_FOUND_INDEX; - return character == nodeArray.mData.get(insertionIndex).mChars[0] ? insertionIndex - : CHARACTER_NOT_FOUND_INDEX; - } - - /** - * Helper method to find a word in a given branch. - */ - @SuppressWarnings("unused") - public static PtNode findWordInTree(PtNodeArray nodeArray, final String string) { - int index = 0; - final StringBuilder checker = DBG ? new StringBuilder() : null; - final int[] codePoints = getCodePoints(string); - - PtNode currentPtNode; - do { - int indexOfGroup = findIndexOfChar(nodeArray, codePoints[index]); - if (CHARACTER_NOT_FOUND_INDEX == indexOfGroup) return null; - currentPtNode = nodeArray.mData.get(indexOfGroup); - - if (codePoints.length - index < currentPtNode.mChars.length) return null; - int newIndex = index; - while (newIndex < codePoints.length && newIndex - index < currentPtNode.mChars.length) { - if (currentPtNode.mChars[newIndex - index] != codePoints[newIndex]) return null; - newIndex++; - } - index = newIndex; - - if (DBG) { - checker.append(new String(currentPtNode.mChars, 0, currentPtNode.mChars.length)); - } - if (index < codePoints.length) { - nodeArray = currentPtNode.mChildren; - } - } while (null != nodeArray && index < codePoints.length); - - if (index < codePoints.length) return null; - if (!currentPtNode.isTerminal()) return null; - if (DBG && !string.equals(checker.toString())) return null; - return currentPtNode; - } - - /** - * Helper method to find out whether a word is in the dict or not. - */ - public boolean hasWord(final String s) { - if (null == s || "".equals(s)) { - throw new RuntimeException("Can't search for a null or empty string"); - } - return null != findWordInTree(mRootNodeArray, s); - } - - /** - * Recursively count the number of PtNodes in a given branch of the trie. - * - * @param nodeArray the parent node. - * @return the number of PtNodes in all the branch under this node. - */ - public static int countPtNodes(final PtNodeArray nodeArray) { - final int nodeSize = nodeArray.mData.size(); - int size = nodeSize; - for (int i = nodeSize - 1; i >= 0; --i) { - PtNode ptNode = nodeArray.mData.get(i); - if (null != ptNode.mChildren) - size += countPtNodes(ptNode.mChildren); - } - return size; - } - - /** - * Recursively count the number of nodes in a given branch of the trie. - * - * @param nodeArray the node array to count. - * @return the number of nodes in this branch. - */ - public static int countNodeArrays(final PtNodeArray nodeArray) { - int size = 1; - for (int i = nodeArray.mData.size() - 1; i >= 0; --i) { - PtNode ptNode = nodeArray.mData.get(i); - if (null != ptNode.mChildren) - size += countNodeArrays(ptNode.mChildren); - } - return size; - } - - // Recursively find out whether there are any bigrams. - // This can be pretty expensive especially if there aren't any (we return as soon - // as we find one, so it's much cheaper if there are bigrams) - private static boolean hasBigramsInternal(final PtNodeArray nodeArray) { - if (null == nodeArray) return false; - for (int i = nodeArray.mData.size() - 1; i >= 0; --i) { - PtNode ptNode = nodeArray.mData.get(i); - if (null != ptNode.mBigrams) return true; - if (hasBigramsInternal(ptNode.mChildren)) return true; - } - return false; - } - - /** - * Finds out whether there are any bigrams in this dictionary. - * - * @return true if there is any bigram, false otherwise. - */ - // TODO: this is expensive especially for large dictionaries without any bigram. - // The up side is, this is always accurate and correct and uses no memory. We should - // find a more efficient way of doing this, without compromising too much on memory - // and ease of use. - public boolean hasBigrams() { - return hasBigramsInternal(mRootNodeArray); - } - - // Historically, the tails of the words were going to be merged to save space. - // However, that would prevent the code to search for a specific address in log(n) - // time so this was abandoned. - // The code is still of interest as it does add some compression to any dictionary - // that has no need for attributes. Implementations that does not read attributes should be - // able to read a dictionary with merged tails. - // Also, the following code does support frequencies, as in, it will only merges - // tails that share the same frequency. Though it would result in the above loss of - // performance while searching by address, it is still technically possible to merge - // tails that contain attributes, but this code does not take that into account - it does - // not compare attributes and will merge terminals with different attributes regardless. - public void mergeTails() { - MakedictLog.i("Do not merge tails"); - return; - -// MakedictLog.i("Merging PtNodes. Number of PtNodes : " + countPtNodes(root)); -// MakedictLog.i("Number of PtNodes : " + countPtNodes(root)); -// -// final HashMap<String, ArrayList<PtNodeArray>> repository = -// new HashMap<String, ArrayList<PtNodeArray>>(); -// mergeTailsInner(repository, root); -// -// MakedictLog.i("Number of different pseudohashes : " + repository.size()); -// int size = 0; -// for (ArrayList<PtNodeArray> a : repository.values()) { -// size += a.size(); -// } -// MakedictLog.i("Number of nodes after merge : " + (1 + size)); -// MakedictLog.i("Recursively seen nodes : " + countNodes(root)); - } - - // The following methods are used by the deactivated mergeTails() -// private static boolean isEqual(PtNodeArray a, PtNodeArray b) { -// if (null == a && null == b) return true; -// if (null == a || null == b) return false; -// if (a.data.size() != b.data.size()) return false; -// final int size = a.data.size(); -// for (int i = size - 1; i >= 0; --i) { -// PtNode aPtNode = a.data.get(i); -// PtNode bPtNode = b.data.get(i); -// if (aPtNode.frequency != bPtNode.frequency) return false; -// if (aPtNode.alternates == null && bPtNode.alternates != null) return false; -// if (aPtNode.alternates != null && !aPtNode.equals(bPtNode.alternates)) return false; -// if (!Arrays.equals(aPtNode.chars, bPtNode.chars)) return false; -// if (!isEqual(aPtNode.children, bPtNode.children)) return false; -// } -// return true; -// } - -// static private HashMap<String, ArrayList<PtNodeArray>> mergeTailsInner( -// final HashMap<String, ArrayList<PtNodeArray>> map, final PtNodeArray nodeArray) { -// final ArrayList<PtNode> branches = nodeArray.data; -// final int nodeSize = branches.size(); -// for (int i = 0; i < nodeSize; ++i) { -// PtNode ptNode = branches.get(i); -// if (null != ptNode.children) { -// String pseudoHash = getPseudoHash(ptNode.children); -// ArrayList<PtNodeArray> similarList = map.get(pseudoHash); -// if (null == similarList) { -// similarList = new ArrayList<PtNodeArray>(); -// map.put(pseudoHash, similarList); -// } -// boolean merged = false; -// for (PtNodeArray similar : similarList) { -// if (isEqual(ptNode.children, similar)) { -// ptNode.children = similar; -// merged = true; -// break; -// } -// } -// if (!merged) { -// similarList.add(ptNode.children); -// } -// mergeTailsInner(map, ptNode.children); -// } -// } -// return map; -// } - -// private static String getPseudoHash(final PtNodeArray nodeArray) { -// StringBuilder s = new StringBuilder(); -// for (PtNode ptNode : nodeArray.data) { -// s.append(ptNode.frequency); -// for (int ch : ptNode.chars) { -// s.append(Character.toChars(ch)); -// } -// } -// return s.toString(); -// } - - /** - * Iterator to walk through a dictionary. - * - * This is purely for convenience. - */ - public static final class DictionaryIterator implements Iterator<Word> { - private static final class Position { - public Iterator<PtNode> pos; - public int length; - public Position(ArrayList<PtNode> ptNodes) { - pos = ptNodes.iterator(); - length = 0; - } - } - final StringBuilder mCurrentString; - final LinkedList<Position> mPositions; - - public DictionaryIterator(ArrayList<PtNode> ptRoot) { - mCurrentString = new StringBuilder(); - mPositions = new LinkedList<Position>(); - final Position rootPos = new Position(ptRoot); - mPositions.add(rootPos); - } - - @Override - public boolean hasNext() { - for (Position p : mPositions) { - if (p.pos.hasNext()) { - return true; - } - } - return false; - } - - @Override - public Word next() { - Position currentPos = mPositions.getLast(); - mCurrentString.setLength(currentPos.length); - - do { - if (currentPos.pos.hasNext()) { - final PtNode currentPtNode = currentPos.pos.next(); - currentPos.length = mCurrentString.length(); - for (int i : currentPtNode.mChars) { - mCurrentString.append(Character.toChars(i)); - } - if (null != currentPtNode.mChildren) { - currentPos = new Position(currentPtNode.mChildren.mData); - currentPos.length = mCurrentString.length(); - mPositions.addLast(currentPos); - } - if (currentPtNode.mFrequency >= 0) { - return new Word(mCurrentString.toString(), currentPtNode.mFrequency, - currentPtNode.mShortcutTargets, currentPtNode.mBigrams, - currentPtNode.mIsNotAWord, currentPtNode.mIsBlacklistEntry); - } - } else { - mPositions.removeLast(); - currentPos = mPositions.getLast(); - mCurrentString.setLength(mPositions.getLast().length); - } - } while (true); - } - - @Override - public void remove() { - throw new UnsupportedOperationException("Unsupported yet"); - } - - } - - /** - * Method to return an iterator. - * - * This method enables Java's enhanced for loop. With this you can have a FusionDictionary x - * and say : for (Word w : x) {} - */ - @Override - public Iterator<Word> iterator() { - return new DictionaryIterator(mRootNodeArray.mData); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java b/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java deleted file mode 100644 index cf07209d9..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import android.util.Log; - -/** - * Wrapper to redirect log events to the right output medium. - */ -public final class MakedictLog { - public static final boolean DBG = false; - private static final String TAG = MakedictLog.class.getSimpleName(); - - public static void d(String message) { - if (DBG) { - Log.d(TAG, message); - } - } - - public static void i(String message) { - if (DBG) { - Log.i(TAG, message); - } - } - - public static void w(String message) { - Log.w(TAG, message); - } - - public static void e(String message) { - Log.e(TAG, message); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java b/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java deleted file mode 100644 index 70e24cc98..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -/** - * A not-yet-resolved attribute. - * - * An attribute is either a bigram or a shortcut. - * All instances of this class are always immutable. - */ -public final class PendingAttribute { - public final int mFrequency; - public final int mAddress; - public PendingAttribute(final int frequency, final int address) { - mFrequency = frequency; - mAddress = address; - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java b/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java new file mode 100644 index 000000000..5fcbb6357 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/ProbabilityInfo.java @@ -0,0 +1,91 @@ +/* + * 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.makedict; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.BinaryDictionary; +import com.android.inputmethod.latin.utils.CombinedFormatUtils; + +import java.util.Arrays; + +public final class ProbabilityInfo { + public final int mProbability; + // mTimestamp, mLevel and mCount are historical info. These values are depend on the + // implementation in native code; thus, we must not use them and have any assumptions about + // them except for tests. + public final int mTimestamp; + public final int mLevel; + public final int mCount; + + @UsedForTesting + public static ProbabilityInfo max(final ProbabilityInfo probabilityInfo1, + final ProbabilityInfo probabilityInfo2) { + if (probabilityInfo1 == null) { + return probabilityInfo2; + } + if (probabilityInfo2 == null) { + return probabilityInfo1; + } + if (probabilityInfo1.mProbability > probabilityInfo2.mProbability) { + return probabilityInfo1; + } else { + return probabilityInfo2; + } + } + + public ProbabilityInfo(final int probability) { + this(probability, BinaryDictionary.NOT_A_VALID_TIMESTAMP, 0, 0); + } + + public ProbabilityInfo(final int probability, final int timestamp, final int level, + final int count) { + mProbability = probability; + mTimestamp = timestamp; + mLevel = level; + mCount = count; + } + + public boolean hasHistoricalInfo() { + return mTimestamp != BinaryDictionary.NOT_A_VALID_TIMESTAMP; + } + + @Override + public int hashCode() { + if (hasHistoricalInfo()) { + return Arrays.hashCode(new Object[] { mProbability, mTimestamp, mLevel, mCount }); + } else { + return Arrays.hashCode(new Object[] { mProbability }); + } + } + + @Override + public String toString() { + return CombinedFormatUtils.formatProbabilityInfo(this); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof ProbabilityInfo)) return false; + final ProbabilityInfo p = (ProbabilityInfo)o; + if (!hasHistoricalInfo() && !p.hasHistoricalInfo()) { + return mProbability == p.mProbability; + } + return mProbability == p.mProbability && mTimestamp == p.mTimestamp && mLevel == p.mLevel + && mCount == p.mCount; + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java b/java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java deleted file mode 100644 index 188de7a0f..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.util.ArrayList; - -/** - * Raw PtNode info straight out of a file. This will contain numbers for addresses. - */ -public final class PtNodeInfo { - - public final int mOriginalAddress; - public final int mEndAddress; - public final int mFlags; - public final int[] mCharacters; - public final int mFrequency; - public final int mChildrenAddress; - public final int mParentAddress; - public final ArrayList<WeightedString> mShortcutTargets; - public final ArrayList<PendingAttribute> mBigrams; - - public PtNodeInfo(final int originalAddress, final int endAddress, final int flags, - final int[] characters, final int frequency, final int parentAddress, - final int childrenAddress, final ArrayList<WeightedString> shortcutTargets, - final ArrayList<PendingAttribute> bigrams) { - mOriginalAddress = originalAddress; - mEndAddress = endAddress; - mFlags = flags; - mCharacters = characters; - mFrequency = frequency; - mParentAddress = parentAddress; - mChildrenAddress = childrenAddress; - mShortcutTargets = shortcutTargets; - mBigrams = bigrams; - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/SparseTable.java b/java/src/com/android/inputmethod/latin/makedict/SparseTable.java deleted file mode 100644 index 7592a0c13..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/SparseTable.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collections; - -/** - * SparseTable is an extensible map from integer to integer. - * This holds one value for every mBlockSize keys, so it uses 1/mBlockSize'th of the full index - * memory. - */ -@UsedForTesting -public class SparseTable { - - /** - * mLookupTable is indexed by terminal ID, containing exactly one entry for every mBlockSize - * terminals. - * It contains at index i = j / mBlockSize the index in each ArrayList in mContentsTables where - * the values for terminals with IDs j to j + mBlockSize - 1 are stored as an mBlockSize-sized - * integer array. - */ - private final ArrayList<Integer> mLookupTable; - private final ArrayList<ArrayList<Integer>> mContentTables; - - private final int mBlockSize; - private final int mContentTableCount; - public static final int NOT_EXIST = -1; - public static final int SIZE_OF_INT_IN_BYTES = 4; - - @UsedForTesting - public SparseTable(final int initialCapacity, final int blockSize, - final int contentTableCount) { - mBlockSize = blockSize; - final int lookupTableSize = initialCapacity / mBlockSize - + (initialCapacity % mBlockSize > 0 ? 1 : 0); - mLookupTable = new ArrayList<Integer>(Collections.nCopies(lookupTableSize, NOT_EXIST)); - mContentTableCount = contentTableCount; - mContentTables = CollectionUtils.newArrayList(); - for (int i = 0; i < mContentTableCount; ++i) { - mContentTables.add(new ArrayList<Integer>()); - } - } - - @UsedForTesting - public SparseTable(final ArrayList<Integer> lookupTable, - final ArrayList<ArrayList<Integer>> contentTables, final int blockSize) { - mBlockSize = blockSize; - mContentTableCount = contentTables.size(); - mLookupTable = lookupTable; - mContentTables = contentTables; - } - - /** - * Converts an byte array to an int array considering each set of 4 bytes is an int stored in - * big-endian. - * The length of byteArray must be a multiple of four. - * Otherwise, IndexOutOfBoundsException will be raised. - */ - @UsedForTesting - private static ArrayList<Integer> convertByteArrayToIntegerArray(final byte[] byteArray) { - final ArrayList<Integer> integerArray = new ArrayList<Integer>(byteArray.length / 4); - for (int i = 0; i < byteArray.length; i += 4) { - int value = 0; - for (int j = i; j < i + 4; ++j) { - value <<= 8; - value |= byteArray[j] & 0xFF; - } - integerArray.add(value); - } - return integerArray; - } - - @UsedForTesting - public int get(final int contentTableIndex, final int index) { - if (!contains(index)) { - return NOT_EXIST; - } - return mContentTables.get(contentTableIndex).get( - mLookupTable.get(index / mBlockSize) + (index % mBlockSize)); - } - - @UsedForTesting - public ArrayList<Integer> getAll(final int index) { - final ArrayList<Integer> ret = CollectionUtils.newArrayList(); - for (int i = 0; i < mContentTableCount; ++i) { - ret.add(get(i, index)); - } - return ret; - } - - @UsedForTesting - public void set(final int contentTableIndex, final int index, final int value) { - if (mLookupTable.get(index / mBlockSize) == NOT_EXIST) { - mLookupTable.set(index / mBlockSize, mContentTables.get(contentTableIndex).size()); - for (int i = 0; i < mContentTableCount; ++i) { - for (int j = 0; j < mBlockSize; ++j) { - mContentTables.get(i).add(NOT_EXIST); - } - } - } - mContentTables.get(contentTableIndex).set( - mLookupTable.get(index / mBlockSize) + (index % mBlockSize), value); - } - - public void remove(final int indexOfContent, final int index) { - set(indexOfContent, index, NOT_EXIST); - } - - @UsedForTesting - public int size() { - return mLookupTable.size() * mBlockSize; - } - - @UsedForTesting - /* package */ int getContentTableSize() { - // This class always has at least one content table. - return mContentTables.get(0).size(); - } - - @UsedForTesting - /* package */ int getLookupTableSize() { - return mLookupTable.size(); - } - - public boolean contains(final int index) { - if (index < 0 || index / mBlockSize >= mLookupTable.size() - || mLookupTable.get(index / mBlockSize) == NOT_EXIST) { - return false; - } - return true; - } - - @UsedForTesting - public void write(final OutputStream lookupOutStream, final OutputStream[] contentOutStreams) - throws IOException { - if (contentOutStreams.length != mContentTableCount) { - throw new RuntimeException(contentOutStreams.length + " streams are given, but the" - + " table has " + mContentTableCount + " content tables."); - } - for (final int index : mLookupTable) { - BinaryDictEncoderUtils.writeUIntToStream(lookupOutStream, index, SIZE_OF_INT_IN_BYTES); - } - - for (int i = 0; i < contentOutStreams.length; ++i) { - for (final int data : mContentTables.get(i)) { - BinaryDictEncoderUtils.writeUIntToStream(contentOutStreams[i], data, - SIZE_OF_INT_IN_BYTES); - } - } - } - - @UsedForTesting - public void writeToFiles(final File lookupTableFile, final File[] contentFiles) - throws IOException { - FileOutputStream lookupTableOutStream = null; - final FileOutputStream[] contentTableOutStreams = new FileOutputStream[mContentTableCount]; - try { - lookupTableOutStream = new FileOutputStream(lookupTableFile); - for (int i = 0; i < contentFiles.length; ++i) { - contentTableOutStreams[i] = new FileOutputStream(contentFiles[i]); - } - write(lookupTableOutStream, contentTableOutStreams); - } finally { - if (lookupTableOutStream != null) { - lookupTableOutStream.close(); - } - for (int i = 0; i < contentTableOutStreams.length; ++i) { - if (contentTableOutStreams[i] != null) { - contentTableOutStreams[i].close(); - } - } - } - } - - private static byte[] readFileToByteArray(final File file) throws IOException { - final byte[] contents = new byte[(int) file.length()]; - FileInputStream inStream = null; - try { - inStream = new FileInputStream(file); - inStream.read(contents); - } finally { - if (inStream != null) { - inStream.close(); - } - } - return contents; - } - - @UsedForTesting - public static SparseTable readFromFiles(final File lookupTableFile, final File[] contentFiles, - final int blockSize) throws IOException { - final ArrayList<ArrayList<Integer>> contentTables = - new ArrayList<ArrayList<Integer>>(contentFiles.length); - for (int i = 0; i < contentFiles.length; ++i) { - contentTables.add(convertByteArrayToIntegerArray(readFileToByteArray(contentFiles[i]))); - } - return new SparseTable(convertByteArrayToIntegerArray(readFileToByteArray(lookupTableFile)), - contentTables, blockSize); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java deleted file mode 100644 index acab4f8a5..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; -import com.android.inputmethod.latin.utils.JniUtils; - -import android.util.Log; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; - -/** - * An implementation of DictDecoder for version 3 binary dictionary. - */ -@UsedForTesting -public class Ver3DictDecoder extends AbstractDictDecoder { - private static final String TAG = Ver3DictDecoder.class.getSimpleName(); - - static { - JniUtils.loadNativeLibrary(); - } - - // TODO: implement something sensical instead of just a phony method - private static native int doNothing(); - - protected static class PtNodeReader extends AbstractDictDecoder.PtNodeReader { - private static int readFrequency(final DictBuffer dictBuffer) { - return dictBuffer.readUnsignedByte(); - } - } - - protected final File mDictionaryBinaryFile; - private final DictionaryBufferFactory mBufferFactory; - protected DictBuffer mDictBuffer; - - /* package */ Ver3DictDecoder(final File file, final int factoryFlag) { - mDictionaryBinaryFile = file; - mDictBuffer = null; - - if ((factoryFlag & MASK_DICTBUFFER) == USE_READONLY_BYTEBUFFER) { - mBufferFactory = new DictionaryBufferFromReadOnlyByteBufferFactory(); - } else if ((factoryFlag & MASK_DICTBUFFER) == USE_BYTEARRAY) { - mBufferFactory = new DictionaryBufferFromByteArrayFactory(); - } else if ((factoryFlag & MASK_DICTBUFFER) == USE_WRITABLE_BYTEBUFFER) { - mBufferFactory = new DictionaryBufferFromWritableByteBufferFactory(); - } else { - mBufferFactory = new DictionaryBufferFromReadOnlyByteBufferFactory(); - } - } - - /* package */ Ver3DictDecoder(final File file, final DictionaryBufferFactory factory) { - mDictionaryBinaryFile = file; - mBufferFactory = factory; - } - - @Override - public void openDictBuffer() throws FileNotFoundException, IOException { - mDictBuffer = mBufferFactory.getDictionaryBuffer(mDictionaryBinaryFile); - } - - @Override - public boolean isDictBufferOpen() { - return mDictBuffer != null; - } - - /* package */ DictBuffer getDictBuffer() { - return mDictBuffer; - } - - @UsedForTesting - /* package */ DictBuffer openAndGetDictBuffer() throws FileNotFoundException, IOException { - openDictBuffer(); - return getDictBuffer(); - } - - @Override - public FileHeader readHeader() throws IOException, UnsupportedFormatException { - if (mDictBuffer == null) { - openDictBuffer(); - } - final FileHeader header = super.readHeader(mDictBuffer); - final int version = header.mFormatOptions.mVersion; - if (!(version >= 2 && version <= 3)) { - throw new UnsupportedFormatException("File header has a wrong version : " + version); - } - return header; - } - - // TODO: Make this buffer multi thread safe. - private final int[] mCharacterBuffer = new int[FormatSpec.MAX_WORD_LENGTH]; - @Override - public PtNodeInfo readPtNode(final int ptNodePos, final FormatOptions options) { - int addressPointer = ptNodePos; - final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); - addressPointer += FormatSpec.PTNODE_FLAGS_SIZE; - - final int parentAddress = PtNodeReader.readParentAddress(mDictBuffer, options); - if (BinaryDictIOUtils.supportsDynamicUpdate(options)) { - addressPointer += FormatSpec.PARENT_ADDRESS_SIZE; - } - - final int characters[]; - if (0 != (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS)) { - int index = 0; - int character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - while (FormatSpec.INVALID_CHARACTER != character) { - // FusionDictionary is making sure that the length of the word is smaller than - // MAX_WORD_LENGTH. - // So we'll never write past the end of mCharacterBuffer. - mCharacterBuffer[index++] = character; - character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - } - characters = Arrays.copyOfRange(mCharacterBuffer, 0, index); - } else { - final int character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - characters = new int[] { character }; - } - final int frequency; - if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) { - frequency = PtNodeReader.readFrequency(mDictBuffer); - addressPointer += FormatSpec.PTNODE_FREQUENCY_SIZE; - } else { - frequency = PtNode.NOT_A_TERMINAL; - } - int childrenAddress = PtNodeReader.readChildrenAddress(mDictBuffer, flags, options); - if (childrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { - childrenAddress += addressPointer; - } - addressPointer += BinaryDictIOUtils.getChildrenAddressSize(flags, options); - final ArrayList<WeightedString> shortcutTargets; - if (0 != (flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)) { - // readShortcut will add shortcuts to shortcutTargets. - shortcutTargets = new ArrayList<WeightedString>(); - addressPointer += PtNodeReader.readShortcut(mDictBuffer, shortcutTargets); - } else { - shortcutTargets = null; - } - - final ArrayList<PendingAttribute> bigrams; - if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) { - bigrams = new ArrayList<PendingAttribute>(); - addressPointer += PtNodeReader.readBigramAddresses(mDictBuffer, bigrams, - addressPointer); - if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - throw new RuntimeException("Too many bigrams in a PtNode (" + bigrams.size() - + " but max is " + FormatSpec.MAX_BIGRAMS_IN_A_PTNODE + ")"); - } - } else { - bigrams = null; - } - return new PtNodeInfo(ptNodePos, addressPointer, flags, characters, frequency, - parentAddress, childrenAddress, shortcutTargets, bigrams); - } - - @Override - public FusionDictionary readDictionaryBinary(final FusionDictionary dict, - final boolean deleteDictIfBroken) - throws FileNotFoundException, IOException, UnsupportedFormatException { - if (mDictBuffer == null) { - openDictBuffer(); - } - try { - return BinaryDictDecoderUtils.readDictionaryBinary(this, dict); - } catch (IOException e) { - Log.e(TAG, "The dictionary " + mDictionaryBinaryFile.getName() + " is broken.", e); - if (deleteDictIfBroken && !mDictionaryBinaryFile.delete()) { - Log.e(TAG, "Failed to delete the broken dictionary."); - } - throw e; - } catch (UnsupportedFormatException e) { - Log.e(TAG, "The dictionary " + mDictionaryBinaryFile.getName() + " is broken.", e); - if (deleteDictIfBroken && !mDictionaryBinaryFile.delete()) { - Log.e(TAG, "Failed to delete the broken dictionary."); - } - throw e; - } - } - - @Override - public void setPosition(int newPos) { - mDictBuffer.position(newPos); - } - - @Override - public int getPosition() { - return mDictBuffer.position(); - } - - @Override - public int readPtNodeCount() { - return BinaryDictDecoderUtils.readPtNodeCount(mDictBuffer); - } - - @Override - public boolean readAndFollowForwardLink() { - final int nextAddress = mDictBuffer.readUnsignedInt24(); - if (nextAddress >= 0 && nextAddress < mDictBuffer.limit()) { - mDictBuffer.position(nextAddress); - return true; - } - return false; - } - - @Override - public boolean hasNextPtNodeArray() { - return mDictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS; - } - - @Override - public void skipPtNode(final FormatOptions formatOptions) { - final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); - PtNodeReader.readParentAddress(mDictBuffer, formatOptions); - BinaryDictIOUtils.skipString(mDictBuffer, - (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0); - PtNodeReader.readChildrenAddress(mDictBuffer, flags, formatOptions); - if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) PtNodeReader.readFrequency(mDictBuffer); - if ((flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS) != 0) { - final int shortcutsSize = mDictBuffer.readUnsignedShort(); - mDictBuffer.position(mDictBuffer.position() + shortcutsSize - - FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE); - } - if ((flags & FormatSpec.FLAG_HAS_BIGRAMS) != 0) { - int bigramCount = 0; - while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - final int bigramFlags = mDictBuffer.readUnsignedByte(); - switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) { - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE: - mDictBuffer.readUnsignedByte(); - break; - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES: - mDictBuffer.readUnsignedShort(); - break; - case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES: - mDictBuffer.readUnsignedInt24(); - break; - } - if ((bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT) == 0) break; - } - if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - throw new RuntimeException("Too many bigrams in a PtNode."); - } - } - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java deleted file mode 100644 index 5da34534e..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; - -/** - * An implementation of DictEncoder for version 3 binary dictionary. - */ -public class Ver3DictEncoder implements DictEncoder { - - private final File mDictFile; - private OutputStream mOutStream; - private byte[] mBuffer; - private int mPosition; - - public Ver3DictEncoder(final File dictFile) { - mDictFile = dictFile; - mOutStream = null; - mBuffer = null; - } - - // This constructor is used only by BinaryDictOffdeviceUtilsTests. - // If you want to use this in the production code, you should consider keeping consistency of - // the interface of Ver3DictDecoder by using factory. - public Ver3DictEncoder(final OutputStream outStream) { - mDictFile = null; - mOutStream = outStream; - } - - private void openStream() throws FileNotFoundException { - mOutStream = new FileOutputStream(mDictFile); - } - - private void close() throws IOException { - if (mOutStream != null) { - mOutStream.close(); - mOutStream = null; - } - } - - @Override - public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions) - throws IOException, UnsupportedFormatException { - if (formatOptions.mVersion > FormatSpec.VERSION3) { - throw new UnsupportedFormatException( - "The given format options has wrong version number : " - + formatOptions.mVersion); - } - - if (mOutStream == null) { - openStream(); - } - BinaryDictEncoderUtils.writeDictionaryHeader(mOutStream, dict, formatOptions); - - // Addresses are limited to 3 bytes, but since addresses can be relative to each node - // array, the structure itself is not limited to 16MB. However, if it is over 16MB deciding - // the order of the PtNode arrays becomes a quite complicated problem, because though the - // dictionary itself does not have a size limit, each node array must still be within 16MB - // of all its children and parents. As long as this is ensured, the dictionary file may - // grow to any size. - - // Leave the choice of the optimal node order to the flattenTree function. - MakedictLog.i("Flattening the tree..."); - ArrayList<PtNodeArray> flatNodes = BinaryDictEncoderUtils.flattenTree(dict.mRootNodeArray); - - MakedictLog.i("Computing addresses..."); - BinaryDictEncoderUtils.computeAddresses(dict, flatNodes, formatOptions); - MakedictLog.i("Checking PtNode array..."); - if (MakedictLog.DBG) BinaryDictEncoderUtils.checkFlatPtNodeArrayList(flatNodes); - - // Create a buffer that matches the final dictionary size. - final PtNodeArray lastNodeArray = flatNodes.get(flatNodes.size() - 1); - final int bufferSize = lastNodeArray.mCachedAddressAfterUpdate + lastNodeArray.mCachedSize; - mBuffer = new byte[bufferSize]; - - MakedictLog.i("Writing file..."); - - for (PtNodeArray nodeArray : flatNodes) { - BinaryDictEncoderUtils.writePlacedPtNodeArray(dict, this, nodeArray, formatOptions); - } - if (MakedictLog.DBG) BinaryDictEncoderUtils.showStatistics(flatNodes); - mOutStream.write(mBuffer, 0, mPosition); - - MakedictLog.i("Done"); - close(); - } - - @Override - public void setPosition(final int position) { - if (mBuffer == null || position < 0 || position >= mBuffer.length) return; - mPosition = position; - } - - @Override - public int getPosition() { - return mPosition; - } - - @Override - public void writePtNodeCount(final int ptNodeCount) { - final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount); - if (countSize != 1 && countSize != 2) { - throw new RuntimeException("Strange size from getGroupCountSize : " + countSize); - } - final int encodedPtNodeCount = (countSize == 2) ? - (ptNodeCount | FormatSpec.LARGE_PTNODE_ARRAY_SIZE_FIELD_SIZE_FLAG) : ptNodeCount; - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, encodedPtNodeCount, - countSize); - } - - private void writePtNodeFlags(final PtNode ptNode, final FormatOptions formatOptions) { - final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, - BinaryDictEncoderUtils.makePtNodeFlags(ptNode, childrenPos, formatOptions), - FormatSpec.PTNODE_FLAGS_SIZE); - } - - private void writeParentPosition(final int parentPosition, final PtNode ptNode, - final FormatOptions formatOptions) { - if (parentPosition == FormatSpec.NO_PARENT_ADDRESS) { - mPosition = BinaryDictEncoderUtils.writeParentAddress(mBuffer, mPosition, - parentPosition, formatOptions); - } else { - mPosition = BinaryDictEncoderUtils.writeParentAddress(mBuffer, mPosition, - parentPosition - ptNode.mCachedAddressAfterUpdate, formatOptions); - } - } - - private void writeCharacters(final int[] codePoints, final boolean hasSeveralChars) { - mPosition = CharEncoding.writeCharArray(codePoints, mBuffer, mPosition); - if (hasSeveralChars) { - mBuffer[mPosition++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR; - } - } - - private void writeFrequency(final int frequency) { - if (frequency >= 0) { - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, frequency, - FormatSpec.PTNODE_FREQUENCY_SIZE); - } - } - - private void writeChildrenPosition(final PtNode ptNode, final FormatOptions formatOptions) { - final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); - if (formatOptions.mSupportsDynamicUpdate) { - mPosition += BinaryDictEncoderUtils.writeSignedChildrenPosition(mBuffer, mPosition, - childrenPos); - } else { - mPosition += BinaryDictEncoderUtils.writeChildrenPosition(mBuffer, mPosition, - childrenPos); - } - } - - /** - * Write a shortcut attributes list to mBuffer. - * - * @param shortcuts the shortcut attributes list. - */ - private void writeShortcuts(final ArrayList<WeightedString> shortcuts) { - if (null == shortcuts || shortcuts.isEmpty()) return; - - final int indexOfShortcutByteSize = mPosition; - mPosition += FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE; - final Iterator<WeightedString> shortcutIterator = shortcuts.iterator(); - while (shortcutIterator.hasNext()) { - final WeightedString target = shortcutIterator.next(); - final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags( - shortcutIterator.hasNext(), - target.mFrequency); - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, shortcutFlags, - FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - final int shortcutShift = CharEncoding.writeString(mBuffer, mPosition, target.mWord); - mPosition += shortcutShift; - } - final int shortcutByteSize = mPosition - indexOfShortcutByteSize; - if (shortcutByteSize > FormatSpec.MAX_SHORTCUT_LIST_SIZE_IN_A_PTNODE) { - throw new RuntimeException("Shortcut list too large"); - } - BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, indexOfShortcutByteSize, shortcutByteSize, - FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE); - } - - /** - * Write a bigram attributes list to mBuffer. - * - * @param bigrams the bigram attributes list. - * @param dict the dictionary the node array is a part of (for relative offsets). - */ - private void writeBigrams(final ArrayList<WeightedString> bigrams, - final FusionDictionary dict) { - if (bigrams == null) return; - - final Iterator<WeightedString> bigramIterator = bigrams.iterator(); - while (bigramIterator.hasNext()) { - final WeightedString bigram = bigramIterator.next(); - final PtNode target = - FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord); - final int addressOfBigram = target.mCachedAddressAfterUpdate; - final int unigramFrequencyForThisWord = target.mFrequency; - final int offset = addressOfBigram - - (mPosition + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - final int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(bigramIterator.hasNext(), - offset, bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord); - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, bigramFlags, - FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - mPosition += BinaryDictEncoderUtils.writeChildrenPosition(mBuffer, mPosition, - Math.abs(offset)); - } - } - - @Override - public void writeForwardLinkAddress(final int forwardLinkAddress) { - mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, forwardLinkAddress, - FormatSpec.FORWARD_LINK_ADDRESS_SIZE); - } - - @Override - public void writePtNode(final PtNode ptNode, final int parentPosition, - final FormatOptions formatOptions, final FusionDictionary dict) { - writePtNodeFlags(ptNode, formatOptions); - writeParentPosition(parentPosition, ptNode, formatOptions); - writeCharacters(ptNode.mChars, ptNode.hasSeveralChars()); - writeFrequency(ptNode.mFrequency); - writeChildrenPosition(ptNode, formatOptions); - writeShortcuts(ptNode.mShortcutTargets); - writeBigrams(ptNode.mBigrams, dict); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java deleted file mode 100644 index 07adda625..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver3DictUpdater.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; - -/** - * An implementation of DictUpdater for version 3 binary dictionary. - */ -@UsedForTesting -public class Ver3DictUpdater extends Ver3DictDecoder implements DictUpdater { - private OutputStream mOutStream; - - @UsedForTesting - public Ver3DictUpdater(final File dictFile, final int factoryType) { - // DictUpdater must have an updatable DictBuffer. - super(dictFile, ((factoryType & MASK_DICTBUFFER) == USE_BYTEARRAY) - ? USE_BYTEARRAY : USE_WRITABLE_BYTEBUFFER); - mOutStream = null; - } - - private void openStreamAndBuffer() throws FileNotFoundException, IOException { - super.openDictBuffer(); - mOutStream = new FileOutputStream(mDictionaryBinaryFile, true /* append */); - } - - private void close() throws IOException { - if (mOutStream != null) { - mOutStream.close(); - mOutStream = null; - } - } - - @Override @UsedForTesting - public void deleteWord(final String word) throws IOException, UnsupportedFormatException { - if (mOutStream == null) openStreamAndBuffer(); - mDictBuffer.position(0); - readHeader(); - final int wordPos = getTerminalPosition(word); - if (wordPos != FormatSpec.NOT_VALID_WORD) { - mDictBuffer.position(wordPos); - final int flags = mDictBuffer.readUnsignedByte(); - mDictBuffer.position(wordPos); - mDictBuffer.put((byte) DynamicBinaryDictIOUtils.markAsDeleted(flags)); - } - close(); - } - - @Override @UsedForTesting - public void insertWord(final String word, final int frequency, - final ArrayList<WeightedString> bigramStrings, - final ArrayList<WeightedString> shortcuts, - final boolean isNotAWord, final boolean isBlackListEntry) - throws IOException, UnsupportedFormatException { - if (mOutStream == null) openStreamAndBuffer(); - DynamicBinaryDictIOUtils.insertWord(this, mOutStream, word, frequency, bigramStrings, - shortcuts, isNotAWord, isBlackListEntry); - close(); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java deleted file mode 100644 index 734223ec2..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import android.util.Log; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; - -/** - * An implementation of binary dictionary decoder for version 4 binary dictionary. - */ -@UsedForTesting -public class Ver4DictDecoder extends AbstractDictDecoder { - private static final String TAG = Ver4DictDecoder.class.getSimpleName(); - - private static final int FILETYPE_TRIE = 1; - private static final int FILETYPE_FREQUENCY = 2; - private static final int FILETYPE_TERMINAL_ADDRESS_TABLE = 3; - private static final int FILETYPE_BIGRAM_FREQ = 4; - private static final int FILETYPE_SHORTCUT = 5; - - private final File mDictDirectory; - private final DictionaryBufferFactory mBufferFactory; - protected DictBuffer mDictBuffer; - private DictBuffer mFrequencyBuffer; - private DictBuffer mTerminalAddressTableBuffer; - private DictBuffer mBigramBuffer; - private DictBuffer mShortcutBuffer; - private SparseTable mBigramAddressTable; - private SparseTable mShortcutAddressTable; - - @UsedForTesting - /* package */ Ver4DictDecoder(final File dictDirectory, final int factoryFlag) { - mDictDirectory = dictDirectory; - mDictBuffer = mFrequencyBuffer = null; - - if ((factoryFlag & MASK_DICTBUFFER) == USE_READONLY_BYTEBUFFER) { - mBufferFactory = new DictionaryBufferFromReadOnlyByteBufferFactory(); - } else if ((factoryFlag & MASK_DICTBUFFER) == USE_BYTEARRAY) { - mBufferFactory = new DictionaryBufferFromByteArrayFactory(); - } else if ((factoryFlag & MASK_DICTBUFFER) == USE_WRITABLE_BYTEBUFFER) { - mBufferFactory = new DictionaryBufferFromWritableByteBufferFactory(); - } else { - mBufferFactory = new DictionaryBufferFromReadOnlyByteBufferFactory(); - } - } - - @UsedForTesting - /* package */ Ver4DictDecoder(final File dictDirectory, final DictionaryBufferFactory factory) { - mDictDirectory = dictDirectory; - mBufferFactory = factory; - mDictBuffer = mFrequencyBuffer = null; - } - - private File getFile(final int fileType) { - if (fileType == FILETYPE_TRIE) { - return new File(mDictDirectory, - mDictDirectory.getName() + FormatSpec.TRIE_FILE_EXTENSION); - } else if (fileType == FILETYPE_FREQUENCY) { - return new File(mDictDirectory, - mDictDirectory.getName() + FormatSpec.FREQ_FILE_EXTENSION); - } else if (fileType == FILETYPE_TERMINAL_ADDRESS_TABLE) { - return new File(mDictDirectory, - mDictDirectory.getName() + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION); - } else if (fileType == FILETYPE_BIGRAM_FREQ) { - return new File(mDictDirectory, - mDictDirectory.getName() + FormatSpec.BIGRAM_FILE_EXTENSION - + FormatSpec.BIGRAM_FREQ_CONTENT_ID); - } else if (fileType == FILETYPE_SHORTCUT) { - return new File(mDictDirectory, - mDictDirectory.getName() + FormatSpec.SHORTCUT_FILE_EXTENSION - + FormatSpec.SHORTCUT_CONTENT_ID); - } else { - throw new RuntimeException("Unsupported kind of file : " + fileType); - } - } - - @Override - public void openDictBuffer() throws FileNotFoundException, IOException { - mDictBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_TRIE)); - mFrequencyBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_FREQUENCY)); - mTerminalAddressTableBuffer = mBufferFactory.getDictionaryBuffer( - getFile(FILETYPE_TERMINAL_ADDRESS_TABLE)); - mBigramBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_BIGRAM_FREQ)); - loadBigramAddressSparseTable(); - mShortcutBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_SHORTCUT)); - loadShortcutAddressSparseTable(); - } - - @Override - public boolean isDictBufferOpen() { - return mDictBuffer != null; - } - - /* package */ DictBuffer getDictBuffer() { - return mDictBuffer; - } - - @Override - public FileHeader readHeader() throws IOException, UnsupportedFormatException { - if (mDictBuffer == null) { - openDictBuffer(); - } - final FileHeader header = super.readHeader(mDictBuffer); - final int version = header.mFormatOptions.mVersion; - if (version != 4) { - throw new UnsupportedFormatException("File header has a wrong version : " + version); - } - return header; - } - - private void loadBigramAddressSparseTable() throws IOException { - final File lookupIndexFile = new File(mDictDirectory, mDictDirectory.getName() - + FormatSpec.BIGRAM_FILE_EXTENSION + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX); - final File freqsFile = new File(mDictDirectory, mDictDirectory.getName() - + FormatSpec.BIGRAM_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX - + FormatSpec.BIGRAM_FREQ_CONTENT_ID); - mBigramAddressTable = SparseTable.readFromFiles(lookupIndexFile, new File[] { freqsFile }, - FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE); - } - - // TODO: Let's have something like SparseTableContentsReader in this class. - private void loadShortcutAddressSparseTable() throws IOException { - final File lookupIndexFile = new File(mDictDirectory, mDictDirectory.getName() - + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX); - final File contentFile = new File(mDictDirectory, mDictDirectory.getName() - + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX - + FormatSpec.SHORTCUT_CONTENT_ID); - final File timestampsFile = new File(mDictDirectory, mDictDirectory.getName() - + FormatSpec.SHORTCUT_FILE_EXTENSION + FormatSpec.CONTENT_TABLE_FILE_SUFFIX - + FormatSpec.SHORTCUT_CONTENT_ID); - mShortcutAddressTable = SparseTable.readFromFiles(lookupIndexFile, - new File[] { contentFile, timestampsFile }, - FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE); - } - - protected static class PtNodeReader extends AbstractDictDecoder.PtNodeReader { - protected static int readFrequency(final DictBuffer frequencyBuffer, final int terminalId) { - frequencyBuffer.position(terminalId * FormatSpec.FREQUENCY_AND_FLAGS_SIZE + 1); - return frequencyBuffer.readUnsignedByte(); - } - - protected static int readTerminalId(final DictBuffer dictBuffer) { - return dictBuffer.readInt(); - } - } - - private ArrayList<WeightedString> readShortcuts(final int terminalId) { - if (mShortcutAddressTable.get(0, terminalId) == SparseTable.NOT_EXIST) return null; - - final ArrayList<WeightedString> ret = CollectionUtils.newArrayList(); - final int posOfShortcuts = mShortcutAddressTable.get(FormatSpec.SHORTCUT_CONTENT_INDEX, - terminalId); - mShortcutBuffer.position(posOfShortcuts); - while (true) { - final int flags = mShortcutBuffer.readUnsignedByte(); - final String word = CharEncoding.readString(mShortcutBuffer); - ret.add(new WeightedString(word, - flags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY)); - if (0 == (flags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break; - } - return ret; - } - - // TODO: Make this buffer thread safe. - // TODO: Support words longer than FormatSpec.MAX_WORD_LENGTH. - private final int[] mCharacterBuffer = new int[FormatSpec.MAX_WORD_LENGTH]; - @Override - public PtNodeInfo readPtNode(int ptNodePos, FormatOptions options) { - int addressPointer = ptNodePos; - final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); - addressPointer += FormatSpec.PTNODE_FLAGS_SIZE; - - final int parentAddress = PtNodeReader.readParentAddress(mDictBuffer, options); - if (BinaryDictIOUtils.supportsDynamicUpdate(options)) { - addressPointer += FormatSpec.PARENT_ADDRESS_SIZE; - } - - final int characters[]; - if (0 != (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS)) { - int index = 0; - int character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - while (FormatSpec.INVALID_CHARACTER != character - && index < FormatSpec.MAX_WORD_LENGTH) { - mCharacterBuffer[index++] = character; - character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - } - characters = Arrays.copyOfRange(mCharacterBuffer, 0, index); - } else { - final int character = CharEncoding.readChar(mDictBuffer); - addressPointer += CharEncoding.getCharSize(character); - characters = new int[] { character }; - } - final int terminalId; - if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) { - terminalId = PtNodeReader.readTerminalId(mDictBuffer); - addressPointer += FormatSpec.PTNODE_TERMINAL_ID_SIZE; - } else { - terminalId = PtNode.NOT_A_TERMINAL; - } - - final int frequency; - if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) { - frequency = PtNodeReader.readFrequency(mFrequencyBuffer, terminalId); - } else { - frequency = PtNode.NOT_A_TERMINAL; - } - int childrenAddress = PtNodeReader.readChildrenAddress(mDictBuffer, flags, options); - if (childrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) { - childrenAddress += addressPointer; - } - addressPointer += BinaryDictIOUtils.getChildrenAddressSize(flags, options); - final ArrayList<WeightedString> shortcutTargets = readShortcuts(terminalId); - - final ArrayList<PendingAttribute> bigrams; - if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) { - bigrams = new ArrayList<PendingAttribute>(); - final int posOfBigrams = mBigramAddressTable.get(0 /* contentTableIndex */, terminalId); - mBigramBuffer.position(posOfBigrams); - while (bigrams.size() < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - // If bigrams.size() reaches FormatSpec.MAX_BIGRAMS_IN_A_PTNODE, - // remaining bigram entries are ignored. - final int bigramFlags = mBigramBuffer.readUnsignedByte(); - final int targetTerminalId = mBigramBuffer.readUnsignedInt24(); - mTerminalAddressTableBuffer.position( - targetTerminalId * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE); - final int targetAddress = mTerminalAddressTableBuffer.readUnsignedInt24(); - bigrams.add(new PendingAttribute( - bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY, - targetAddress)); - if (0 == (bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT)) break; - } - if (bigrams.size() >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) { - throw new RuntimeException("Too many bigrams in a PtNode (" + bigrams.size() - + " but max is " + FormatSpec.MAX_BIGRAMS_IN_A_PTNODE + ")"); - } - } else { - bigrams = null; - } - return new PtNodeInfo(ptNodePos, addressPointer, flags, characters, frequency, - parentAddress, childrenAddress, shortcutTargets, bigrams); - } - - private void deleteDictFiles() { - final File[] files = mDictDirectory.listFiles(); - for (int i = 0; i < files.length; ++i) { - files[i].delete(); - } - } - - @Override - public FusionDictionary readDictionaryBinary(final FusionDictionary dict, - final boolean deleteDictIfBroken) - throws FileNotFoundException, IOException, UnsupportedFormatException { - if (mDictBuffer == null) { - openDictBuffer(); - } - try { - return BinaryDictDecoderUtils.readDictionaryBinary(this, dict); - } catch (IOException e) { - Log.e(TAG, "The dictionary " + mDictDirectory.getName() + " is broken.", e); - if (deleteDictIfBroken) { - deleteDictFiles(); - } - throw e; - } catch (UnsupportedFormatException e) { - Log.e(TAG, "The dictionary " + mDictDirectory.getName() + " is broken.", e); - if (deleteDictIfBroken) { - deleteDictFiles(); - } - throw e; - } - } - - @Override - public void setPosition(int newPos) { - mDictBuffer.position(newPos); - } - - @Override - public int getPosition() { - return mDictBuffer.position(); - } - - @Override - public int readPtNodeCount() { - return BinaryDictDecoderUtils.readPtNodeCount(mDictBuffer); - } - - @Override - public boolean readAndFollowForwardLink() { - final int nextAddress = mDictBuffer.readUnsignedInt24(); - if (nextAddress >= 0 && nextAddress < mDictBuffer.limit()) { - mDictBuffer.position(nextAddress); - return true; - } - return false; - } - - @Override - public boolean hasNextPtNodeArray() { - return mDictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS; - } - - @Override - public void skipPtNode(final FormatOptions formatOptions) { - final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); - PtNodeReader.readParentAddress(mDictBuffer, formatOptions); - BinaryDictIOUtils.skipString(mDictBuffer, - (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0); - if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) PtNodeReader.readTerminalId(mDictBuffer); - PtNodeReader.readChildrenAddress(mDictBuffer, flags, formatOptions); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java deleted file mode 100644 index 8d5b48a9b..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java +++ /dev/null @@ -1,475 +0,0 @@ -/* -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.CharEncoding; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; - -/** - * An implementation of DictEncoder for version 4 binary dictionary. - */ -@UsedForTesting -public class Ver4DictEncoder implements DictEncoder { - private final File mDictPlacedDir; - private byte[] mTrieBuf; - private int mTriePos; - private int mHeaderSize; - private OutputStream mTrieOutStream; - private OutputStream mFreqOutStream; - private OutputStream mUnigramTimestampOutStream; - private OutputStream mTerminalAddressTableOutStream; - private File mDictDir; - private String mBaseFilename; - private BigramContentWriter mBigramWriter; - private ShortcutContentWriter mShortcutWriter; - - @UsedForTesting - public Ver4DictEncoder(final File dictPlacedDir) { - mDictPlacedDir = dictPlacedDir; - } - - private interface SparseTableContentWriterInterface { - public void write(final OutputStream outStream) throws IOException; - } - - private static class SparseTableContentWriter { - private final int mContentCount; - private final SparseTable mSparseTable; - private final File mLookupTableFile; - protected final File mBaseDir; - private final File[] mAddressTableFiles; - private final File[] mContentFiles; - protected final OutputStream[] mContentOutStreams; - - public SparseTableContentWriter(final String name, final int initialCapacity, - final int blockSize, final File baseDir, final String[] contentFilenames, - final String[] contentIds) { - if (contentFilenames.length != contentIds.length) { - throw new RuntimeException("The length of contentFilenames and the length of" - + " contentIds are different " + contentFilenames.length + ", " - + contentIds.length); - } - mContentCount = contentFilenames.length; - mSparseTable = new SparseTable(initialCapacity, blockSize, mContentCount); - mLookupTableFile = new File(baseDir, name + FormatSpec.LOOKUP_TABLE_FILE_SUFFIX); - mAddressTableFiles = new File[mContentCount]; - mContentFiles = new File[mContentCount]; - mBaseDir = baseDir; - for (int i = 0; i < mContentCount; ++i) { - mAddressTableFiles[i] = new File(mBaseDir, - name + FormatSpec.CONTENT_TABLE_FILE_SUFFIX + contentIds[i]); - mContentFiles[i] = new File(mBaseDir, contentFilenames[i] + contentIds[i]); - } - mContentOutStreams = new OutputStream[mContentCount]; - } - - public void openStreams() throws FileNotFoundException { - for (int i = 0; i < mContentCount; ++i) { - mContentOutStreams[i] = new FileOutputStream(mContentFiles[i]); - } - } - - protected void write(final int contentIndex, final int index, - final SparseTableContentWriterInterface writer) throws IOException { - mSparseTable.set(contentIndex, index, (int) mContentFiles[contentIndex].length()); - writer.write(mContentOutStreams[contentIndex]); - mContentOutStreams[contentIndex].flush(); - } - - public void closeStreams() throws IOException { - mSparseTable.writeToFiles(mLookupTableFile, mAddressTableFiles); - for (int i = 0; i < mContentCount; ++i) { - mContentOutStreams[i].close(); - } - } - } - - private static class BigramContentWriter extends SparseTableContentWriter { - private final boolean mWriteTimestamp; - - public BigramContentWriter(final String name, final int initialCapacity, - final File baseDir, final boolean writeTimestamp) { - super(name + FormatSpec.BIGRAM_FILE_EXTENSION, initialCapacity, - FormatSpec.BIGRAM_ADDRESS_TABLE_BLOCK_SIZE, baseDir, - getContentFilenames(name, writeTimestamp), getContentIds(writeTimestamp)); - mWriteTimestamp = writeTimestamp; - } - - private static String[] getContentFilenames(final String name, - final boolean writeTimestamp) { - final String[] contentFilenames; - if (writeTimestamp) { - contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION, - name + FormatSpec.BIGRAM_FILE_EXTENSION }; - } else { - contentFilenames = new String[] { name + FormatSpec.BIGRAM_FILE_EXTENSION }; - } - return contentFilenames; - } - - private static String[] getContentIds(final boolean writeTimestamp) { - final String[] contentIds; - if (writeTimestamp) { - contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID, - FormatSpec.BIGRAM_TIMESTAMP_CONTENT_ID }; - } else { - contentIds = new String[] { FormatSpec.BIGRAM_FREQ_CONTENT_ID }; - } - return contentIds; - } - - public void writeBigramsForOneWord(final int terminalId, final int bigramCount, - final Iterator<WeightedString> bigramIterator, final FusionDictionary dict) - throws IOException { - write(FormatSpec.BIGRAM_FREQ_CONTENT_INDEX, terminalId, - new SparseTableContentWriterInterface() { - @Override - public void write(final OutputStream outStream) throws IOException { - writeBigramsForOneWordInternal(outStream, bigramIterator, dict); - }}); - if (mWriteTimestamp) { - write(FormatSpec.BIGRAM_TIMESTAMP_CONTENT_INDEX, terminalId, - new SparseTableContentWriterInterface() { - @Override - public void write(final OutputStream outStream) throws IOException { - initBigramTimestampsCountersAndLevelsForOneWordInternal(outStream, - bigramCount); - }}); - } - } - - private void writeBigramsForOneWordInternal(final OutputStream outStream, - final Iterator<WeightedString> bigramIterator, final FusionDictionary dict) - throws IOException { - while (bigramIterator.hasNext()) { - final WeightedString bigram = bigramIterator.next(); - final PtNode target = - FusionDictionary.findWordInTree(dict.mRootNodeArray, bigram.mWord); - final int unigramFrequencyForThisWord = target.mFrequency; - final int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags( - bigramIterator.hasNext(), 0, bigram.mFrequency, - unigramFrequencyForThisWord, bigram.mWord); - BinaryDictEncoderUtils.writeUIntToStream(outStream, bigramFlags, - FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - BinaryDictEncoderUtils.writeUIntToStream(outStream, target.mTerminalId, - FormatSpec.PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE); - } - } - - private void initBigramTimestampsCountersAndLevelsForOneWordInternal( - final OutputStream outStream, final int bigramCount) throws IOException { - for (int i = 0; i < bigramCount; ++i) { - // TODO: Figure out what initial values should be. - BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */, - FormatSpec.BIGRAM_TIMESTAMP_SIZE); - BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */, - FormatSpec.BIGRAM_COUNTER_SIZE); - BinaryDictEncoderUtils.writeUIntToStream(outStream, 0 /* value */, - FormatSpec.BIGRAM_LEVEL_SIZE); - } - } - } - - private static class ShortcutContentWriter extends SparseTableContentWriter { - public ShortcutContentWriter(final String name, final int initialCapacity, - final File baseDir) { - super(name + FormatSpec.SHORTCUT_FILE_EXTENSION, initialCapacity, - FormatSpec.SHORTCUT_ADDRESS_TABLE_BLOCK_SIZE, baseDir, - new String[] { name + FormatSpec.SHORTCUT_FILE_EXTENSION }, - new String[] { FormatSpec.SHORTCUT_CONTENT_ID }); - } - - public void writeShortcutForOneWord(final int terminalId, - final Iterator<WeightedString> shortcutIterator) throws IOException { - write(FormatSpec.SHORTCUT_CONTENT_INDEX, terminalId, - new SparseTableContentWriterInterface() { - @Override - public void write(final OutputStream outStream) throws IOException { - writeShortcutForOneWordInternal(outStream, shortcutIterator); - } - }); - } - - private void writeShortcutForOneWordInternal(final OutputStream outStream, - final Iterator<WeightedString> shortcutIterator) throws IOException { - while (shortcutIterator.hasNext()) { - final WeightedString target = shortcutIterator.next(); - final int shortcutFlags = BinaryDictEncoderUtils.makeShortcutFlags( - shortcutIterator.hasNext(), target.mFrequency); - BinaryDictEncoderUtils.writeUIntToStream(outStream, shortcutFlags, - FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE); - CharEncoding.writeString(outStream, target.mWord); - } - } - } - - private void openStreams(final FormatOptions formatOptions, final DictionaryOptions dictOptions) - throws FileNotFoundException, IOException { - final FileHeader header = new FileHeader(0, dictOptions, formatOptions); - mBaseFilename = header.getId() + "." + header.getVersion(); - mDictDir = new File(mDictPlacedDir, mBaseFilename); - final File trieFile = new File(mDictDir, mBaseFilename + FormatSpec.TRIE_FILE_EXTENSION); - final File freqFile = new File(mDictDir, mBaseFilename + FormatSpec.FREQ_FILE_EXTENSION); - final File timestampFile = new File(mDictDir, - mBaseFilename + FormatSpec.UNIGRAM_TIMESTAMP_FILE_EXTENSION); - final File terminalAddressTableFile = new File(mDictDir, - mBaseFilename + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION); - if (!mDictDir.isDirectory()) { - if (mDictDir.exists()) mDictDir.delete(); - mDictDir.mkdirs(); - } - mTrieOutStream = new FileOutputStream(trieFile); - mFreqOutStream = new FileOutputStream(freqFile); - mTerminalAddressTableOutStream = new FileOutputStream(terminalAddressTableFile); - if (formatOptions.mHasTimestamp) { - mUnigramTimestampOutStream = new FileOutputStream(timestampFile); - } - } - - private void close() throws IOException { - try { - if (mTrieOutStream != null) { - mTrieOutStream.close(); - } - if (mFreqOutStream != null) { - mFreqOutStream.close(); - } - if (mTerminalAddressTableOutStream != null) { - mTerminalAddressTableOutStream.close(); - } - if (mUnigramTimestampOutStream != null) { - mUnigramTimestampOutStream.close(); - } - } finally { - mTrieOutStream = null; - mFreqOutStream = null; - mTerminalAddressTableOutStream = null; - } - } - - @Override - public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions) - throws IOException, UnsupportedFormatException { - if (formatOptions.mVersion != FormatSpec.VERSION4) { - throw new UnsupportedFormatException("File header has a wrong version number : " - + formatOptions.mVersion); - } - if (!mDictPlacedDir.isDirectory()) { - throw new UnsupportedFormatException("Given path is not a directory."); - } - - if (mTrieOutStream == null) { - openStreams(formatOptions, dict.mOptions); - } - - mHeaderSize = BinaryDictEncoderUtils.writeDictionaryHeader(mTrieOutStream, dict, - formatOptions); - - MakedictLog.i("Flattening the tree..."); - ArrayList<PtNodeArray> flatNodes = BinaryDictEncoderUtils.flattenTree(dict.mRootNodeArray); - int terminalCount = 0; - for (final PtNodeArray array : flatNodes) { - for (final PtNode node : array.mData) { - if (node.isTerminal()) node.mTerminalId = terminalCount++; - } - } - - MakedictLog.i("Computing addresses..."); - BinaryDictEncoderUtils.computeAddresses(dict, flatNodes, formatOptions); - if (MakedictLog.DBG) BinaryDictEncoderUtils.checkFlatPtNodeArrayList(flatNodes); - - writeTerminalData(flatNodes, terminalCount); - if (formatOptions.mHasTimestamp) { - initUnigramTimestamps(terminalCount); - } - mBigramWriter = new BigramContentWriter(mBaseFilename, terminalCount, mDictDir, - formatOptions.mHasTimestamp); - writeBigrams(flatNodes, dict); - mShortcutWriter = new ShortcutContentWriter(mBaseFilename, terminalCount, mDictDir); - writeShortcuts(flatNodes); - - final PtNodeArray lastNodeArray = flatNodes.get(flatNodes.size() - 1); - final int bufferSize = lastNodeArray.mCachedAddressAfterUpdate + lastNodeArray.mCachedSize; - mTrieBuf = new byte[bufferSize]; - - MakedictLog.i("Writing file..."); - for (PtNodeArray nodeArray : flatNodes) { - BinaryDictEncoderUtils.writePlacedPtNodeArray(dict, this, nodeArray, formatOptions); - } - if (MakedictLog.DBG) { - BinaryDictEncoderUtils.showStatistics(flatNodes); - MakedictLog.i("has " + terminalCount + " terminals."); - } - mTrieOutStream.write(mTrieBuf); - - MakedictLog.i("Done"); - close(); - } - - @Override - public void setPosition(int position) { - if (mTrieBuf == null || position < 0 || position >- mTrieBuf.length) return; - mTriePos = position; - } - - @Override - public int getPosition() { - return mTriePos; - } - - @Override - public void writePtNodeCount(int ptNodeCount) { - final int countSize = BinaryDictIOUtils.getPtNodeCountSize(ptNodeCount); - // ptNodeCount must fit on one byte or two bytes. - // Please see comments in FormatSpec - if (countSize != 1 && countSize != 2) { - throw new RuntimeException("Strange size from getPtNodeCountSize : " + countSize); - } - final int encodedPtNodeCount = (countSize == 2) ? - (ptNodeCount | FormatSpec.LARGE_PTNODE_ARRAY_SIZE_FIELD_SIZE_FLAG) : ptNodeCount; - mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, encodedPtNodeCount, - countSize); - } - - private void writePtNodeFlags(final PtNode ptNode, final FormatOptions formatOptions) { - final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); - mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, - BinaryDictEncoderUtils.makePtNodeFlags(ptNode, childrenPos, formatOptions), - FormatSpec.PTNODE_FLAGS_SIZE); - } - - private void writeParentPosition(int parentPos, final PtNode ptNode, - final FormatOptions formatOptions) { - if (parentPos != FormatSpec.NO_PARENT_ADDRESS) { - parentPos -= ptNode.mCachedAddressAfterUpdate; - } - mTriePos = BinaryDictEncoderUtils.writeParentAddress(mTrieBuf, mTriePos, parentPos, - formatOptions); - } - - private void writeCharacters(final int[] characters, final boolean hasSeveralChars) { - mTriePos = CharEncoding.writeCharArray(characters, mTrieBuf, mTriePos); - if (hasSeveralChars) { - mTrieBuf[mTriePos++] = FormatSpec.PTNODE_CHARACTERS_TERMINATOR; - } - } - - private void writeTerminalId(final int terminalId) { - mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, terminalId, - FormatSpec.PTNODE_TERMINAL_ID_SIZE); - } - - private void writeChildrenPosition(PtNode ptNode, FormatOptions formatOptions) { - final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions); - if (formatOptions.mSupportsDynamicUpdate) { - mTriePos += BinaryDictEncoderUtils.writeSignedChildrenPosition(mTrieBuf, - mTriePos, childrenPos); - } else { - mTriePos += BinaryDictEncoderUtils.writeChildrenPosition(mTrieBuf, - mTriePos, childrenPos); - } - } - - private void writeBigrams(final ArrayList<PtNodeArray> flatNodes, final FusionDictionary dict) - throws IOException { - mBigramWriter.openStreams(); - for (final PtNodeArray nodeArray : flatNodes) { - for (final PtNode ptNode : nodeArray.mData) { - if (ptNode.mBigrams != null) { - mBigramWriter.writeBigramsForOneWord(ptNode.mTerminalId, ptNode.mBigrams.size(), - ptNode.mBigrams.iterator(), dict); - } - } - } - mBigramWriter.closeStreams(); - } - - private void writeShortcuts(final ArrayList<PtNodeArray> flatNodes) throws IOException { - mShortcutWriter.openStreams(); - for (final PtNodeArray nodeArray : flatNodes) { - for (final PtNode ptNode : nodeArray.mData) { - if (ptNode.mShortcutTargets != null && !ptNode.mShortcutTargets.isEmpty()) { - mShortcutWriter.writeShortcutForOneWord(ptNode.mTerminalId, - ptNode.mShortcutTargets.iterator()); - } - } - } - mShortcutWriter.closeStreams(); - } - - @Override - public void writeForwardLinkAddress(int forwardLinkAddress) { - mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, - forwardLinkAddress, FormatSpec.FORWARD_LINK_ADDRESS_SIZE); - } - - @Override - public void writePtNode(final PtNode ptNode, final int parentPosition, - final FormatOptions formatOptions, final FusionDictionary dict) { - writePtNodeFlags(ptNode, formatOptions); - writeParentPosition(parentPosition, ptNode, formatOptions); - writeCharacters(ptNode.mChars, ptNode.hasSeveralChars()); - if (ptNode.isTerminal()) { - writeTerminalId(ptNode.mTerminalId); - } - writeChildrenPosition(ptNode, formatOptions); - } - - private void writeTerminalData(final ArrayList<PtNodeArray> flatNodes, - final int terminalCount) throws IOException { - final byte[] freqBuf = new byte[terminalCount * FormatSpec.FREQUENCY_AND_FLAGS_SIZE]; - final byte[] terminalAddressTableBuf = - new byte[terminalCount * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE]; - for (final PtNodeArray nodeArray : flatNodes) { - for (final PtNode ptNode : nodeArray.mData) { - if (ptNode.isTerminal()) { - BinaryDictEncoderUtils.writeUIntToBuffer(freqBuf, - ptNode.mTerminalId * FormatSpec.FREQUENCY_AND_FLAGS_SIZE, - ptNode.mFrequency, FormatSpec.FREQUENCY_AND_FLAGS_SIZE); - BinaryDictEncoderUtils.writeUIntToBuffer(terminalAddressTableBuf, - ptNode.mTerminalId * FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE, - ptNode.mCachedAddressAfterUpdate + mHeaderSize, - FormatSpec.TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE); - } - } - } - mFreqOutStream.write(freqBuf); - mTerminalAddressTableOutStream.write(terminalAddressTableBuf); - } - - private void initUnigramTimestamps(final int terminalCount) throws IOException { - // Initial value of time stamps for each word is 0. - final byte[] unigramTimestampBuf = - new byte[terminalCount * FormatSpec.UNIGRAM_TIMESTAMP_SIZE]; - mUnigramTimestampOutStream.write(unigramTimestampBuf); - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java deleted file mode 100644 index 3d8f186ba..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictUpdater.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; - -/** - * An implementation of DictUpdater for version 4 binary dictionary. - */ -@UsedForTesting -public class Ver4DictUpdater extends Ver4DictDecoder implements DictUpdater { - - @UsedForTesting - public Ver4DictUpdater(final File dictDirectory, final int factoryType) { - // DictUpdater must have an updatable DictBuffer. - super(dictDirectory, ((factoryType & MASK_DICTBUFFER) == USE_BYTEARRAY) - ? USE_BYTEARRAY : USE_WRITABLE_BYTEBUFFER); - } - - @Override - public void deleteWord(final String word) throws IOException, UnsupportedFormatException { - if (mDictBuffer == null) openDictBuffer(); - readHeader(); - final int wordPos = getTerminalPosition(word); - if (wordPos != FormatSpec.NOT_VALID_WORD) { - mDictBuffer.position(wordPos); - final int flags = PtNodeReader.readPtNodeOptionFlags(mDictBuffer); - mDictBuffer.position(wordPos); - mDictBuffer.put((byte) DynamicBinaryDictIOUtils.markAsDeleted(flags)); - } - } - - @Override - public void insertWord(final String word, final int frequency, - final ArrayList<WeightedString> bigramStrings, final ArrayList<WeightedString> shortcuts, - final boolean isNotAWord, final boolean isBlackListEntry) - throws IOException, UnsupportedFormatException { - // TODO: Implement this method. - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/WeightedString.java b/java/src/com/android/inputmethod/latin/makedict/WeightedString.java new file mode 100644 index 000000000..f6782df9e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/WeightedString.java @@ -0,0 +1,62 @@ +/* + * 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.makedict; + +import com.android.inputmethod.annotations.UsedForTesting; + +import java.util.Arrays; + +/** + * A string with a probability. + * + * This represents an "attribute", that is either a bigram or a shortcut. + */ +public final class WeightedString { + public final String mWord; + public ProbabilityInfo mProbabilityInfo; + + public WeightedString(final String word, final int probability) { + this(word, new ProbabilityInfo(probability)); + } + + public WeightedString(final String word, final ProbabilityInfo probabilityInfo) { + mWord = word; + mProbabilityInfo = probabilityInfo; + } + + @UsedForTesting + public int getProbability() { + return mProbabilityInfo.mProbability; + } + + public void setProbability(final int probability) { + mProbabilityInfo = new ProbabilityInfo(probability); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] { mWord, mProbabilityInfo}); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof WeightedString)) return false; + final WeightedString w = (WeightedString)o; + return mWord.equals(w.mWord) && mProbabilityInfo.equals(w.mProbabilityInfo); + } +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/makedict/Word.java b/java/src/com/android/inputmethod/latin/makedict/Word.java deleted file mode 100644 index 0eabb7bf3..000000000 --- a/java/src/com/android/inputmethod/latin/makedict/Word.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.makedict; - -import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString; - -import java.util.ArrayList; -import java.util.Arrays; - -/** - * Utility class for a word with a frequency. - * - * This is chiefly used to iterate a dictionary. - */ -public final class Word implements Comparable<Word> { - public final String mWord; - public final int mFrequency; - public final ArrayList<WeightedString> mShortcutTargets; - public final ArrayList<WeightedString> mBigrams; - public final boolean mIsNotAWord; - public final boolean mIsBlacklistEntry; - - private int mHashCode = 0; - - public Word(final String word, final int frequency, - final ArrayList<WeightedString> shortcutTargets, - final ArrayList<WeightedString> bigrams, - final boolean isNotAWord, final boolean isBlacklistEntry) { - mWord = word; - mFrequency = frequency; - mShortcutTargets = shortcutTargets; - mBigrams = bigrams; - mIsNotAWord = isNotAWord; - mIsBlacklistEntry = isBlacklistEntry; - } - - private static int computeHashCode(Word word) { - return Arrays.hashCode(new Object[] { - word.mWord, - word.mFrequency, - word.mShortcutTargets.hashCode(), - word.mBigrams.hashCode(), - word.mIsNotAWord, - word.mIsBlacklistEntry - }); - } - - /** - * Three-way comparison. - * - * A Word x is greater than a word y if x has a higher frequency. If they have the same - * frequency, they are sorted in lexicographic order. - */ - @Override - public int compareTo(Word w) { - if (mFrequency < w.mFrequency) return 1; - if (mFrequency > w.mFrequency) return -1; - return mWord.compareTo(w.mWord); - } - - /** - * Equality test. - * - * Words are equal if they have the same frequency, the same spellings, and the same - * attributes. - */ - @Override - public boolean equals(Object o) { - if (o == this) return true; - if (!(o instanceof Word)) return false; - Word w = (Word)o; - return mFrequency == w.mFrequency && mWord.equals(w.mWord) - && mShortcutTargets.equals(w.mShortcutTargets) - && mBigrams.equals(w.mBigrams) - && mIsNotAWord == w.mIsNotAWord - && mIsBlacklistEntry == w.mIsBlacklistEntry; - } - - @Override - public int hashCode() { - if (mHashCode == 0) { - mHashCode = computeHashCode(this); - } - return mHashCode; - } -} diff --git a/java/src/com/android/inputmethod/latin/makedict/WordProperty.java b/java/src/com/android/inputmethod/latin/makedict/WordProperty.java new file mode 100644 index 000000000..853392200 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/makedict/WordProperty.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.makedict; + +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.BinaryDictionary; +import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.CombinedFormatUtils; +import com.android.inputmethod.latin.utils.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Utility class for a word with a probability. + * + * This is chiefly used to iterate a dictionary. + */ +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 boolean mIsNotAWord; + public final boolean mIsBlacklistEntry; + public final boolean mHasShortcuts; + public final boolean mHasBigrams; + + private int mHashCode = 0; + + @UsedForTesting + public WordProperty(final String word, final ProbabilityInfo probabilityInfo, + final ArrayList<WeightedString> shortcutTargets, + final ArrayList<WeightedString> bigrams, + final boolean isNotAWord, final boolean isBlacklistEntry) { + mWord = word; + mProbabilityInfo = probabilityInfo; + mShortcutTargets = shortcutTargets; + mBigrams = bigrams; + mIsNotAWord = isNotAWord; + mIsBlacklistEntry = isBlacklistEntry; + mHasBigrams = bigrams != null && !bigrams.isEmpty(); + mHasShortcuts = shortcutTargets != null && !shortcutTargets.isEmpty(); + } + + private static ProbabilityInfo createProbabilityInfoFromArray(final int[] probabilityInfo) { + return new ProbabilityInfo( + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_PROBABILITY_INDEX], + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_TIMESTAMP_INDEX], + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_LEVEL_INDEX], + probabilityInfo[BinaryDictionary.FORMAT_WORD_PROPERTY_COUNT_INDEX]); + } + + // Construct word property using information from native code. + // This represents invalid word when the probability is BinaryDictionary.NOT_A_PROBABILITY. + public WordProperty(final int[] codePoints, final boolean isNotAWord, + final boolean isBlacklisted, final boolean hasBigram, + final boolean hasShortcuts, final int[] probabilityInfo, + final ArrayList<int[]> bigramTargets, final ArrayList<int[]> bigramProbabilityInfo, + final ArrayList<int[]> shortcutTargets, + final ArrayList<Integer> shortcutProbabilities) { + mWord = StringUtils.getStringFromNullTerminatedCodePointArray(codePoints); + mProbabilityInfo = createProbabilityInfoFromArray(probabilityInfo); + mShortcutTargets = CollectionUtils.newArrayList(); + mBigrams = CollectionUtils.newArrayList(); + 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)))); + } + + final int shortcutTargetCount = shortcutTargets.size(); + for (int i = 0; i < shortcutTargetCount; i++) { + final String shortcutTargetString = + StringUtils.getStringFromNullTerminatedCodePointArray(shortcutTargets.get(i)); + mShortcutTargets.add( + new WeightedString(shortcutTargetString, shortcutProbabilities.get(i))); + } + } + + public int getProbability() { + return mProbabilityInfo.mProbability; + } + + private static int computeHashCode(WordProperty word) { + return Arrays.hashCode(new Object[] { + word.mWord, + word.mProbabilityInfo, + word.mShortcutTargets.hashCode(), + word.mBigrams.hashCode(), + word.mIsNotAWord, + word.mIsBlacklistEntry + }); + } + + /** + * Three-way comparison. + * + * A Word x is greater than a word y if x has a higher frequency. If they have the same + * frequency, they are sorted in lexicographic order. + */ + @Override + public int compareTo(final WordProperty w) { + if (getProbability() < w.getProbability()) return 1; + if (getProbability() > w.getProbability()) return -1; + return mWord.compareTo(w.mWord); + } + + /** + * Equality test. + * + * Words are equal if they have the same frequency, the same spellings, and the same + * attributes. + */ + @Override + public boolean equals(Object o) { + if (o == this) return true; + 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) + && mIsNotAWord == w.mIsNotAWord && mIsBlacklistEntry == w.mIsBlacklistEntry + && mHasBigrams == w.mHasBigrams && mHasShortcuts && w.mHasBigrams; + } + + @Override + public int hashCode() { + if (mHashCode == 0) { + mHashCode = computeHashCode(this); + } + return mHashCode; + } + + @UsedForTesting + public boolean isValid() { + return getProbability() != BinaryDictionary.NOT_A_PROBABILITY; + } + + @Override + public String toString() { + return CombinedFormatUtils.formatWordProperty(this); + } +} diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java index 1de15a333..712e314a8 100644 --- a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java +++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java @@ -17,25 +17,17 @@ package com.android.inputmethod.latin.personalization; import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; 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.LatinImeLogger; -import com.android.inputmethod.latin.makedict.DictDecoder; -import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.settings.Settings; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils; -import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.OnAddWordListener; +import com.android.inputmethod.latin.makedict.DictionaryHeader; +import com.android.inputmethod.latin.utils.LanguageModelParam; import java.io.File; -import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Locale; import java.util.Map; /** @@ -44,9 +36,7 @@ import java.util.Map; */ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableBinaryDictionary { private static final String TAG = DecayingExpandableBinaryDictionaryBase.class.getSimpleName(); - public static final boolean DBG_SAVE_RESTORE = false; - private static final boolean DBG_STRESS_TEST = false; - private static final boolean PROFILE_SAVE_RESTORE = LatinImeLogger.sDBG; + private static final boolean DBG_DUMP_ON_CLOSE = false; /** Any pair being typed or picked */ public static final int FREQUENCY_FOR_TYPED = 2; @@ -54,63 +44,63 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB public static final int FREQUENCY_FOR_WORDS_IN_DICTS = FREQUENCY_FOR_TYPED; public static final int FREQUENCY_FOR_WORDS_NOT_IN_DICTS = Dictionary.NOT_A_PROBABILITY; - /** Locale for which this user history dictionary is storing words */ - private final String mLocale; + /** The locale for this dictionary. */ + public final Locale mLocale; - private final String mFileName; + private Map<String, String> mAdditionalAttributeMap = null; - private final SharedPreferences mPrefs; - - private final ArrayList<PersonalizationDictionaryUpdateSession> mSessions = - CollectionUtils.newArrayList(); - - // Should always be false except when we use this class for test - @UsedForTesting boolean mIsTest = false; - - /* package */ DecayingExpandableBinaryDictionaryBase(final Context context, - final String locale, final SharedPreferences sp, final String dictionaryType, - final String fileName) { - super(context, fileName, dictionaryType, true); + protected DecayingExpandableBinaryDictionaryBase(final Context context, + final String dictName, final Locale locale, final String dictionaryType, + final File dictFile) { + super(context, dictName, locale, dictionaryType, dictFile); mLocale = locale; - mFileName = fileName; - mPrefs = sp; - if (mLocale != null && mLocale.length() > 1) { - asyncLoadDictionaryToMemory(); + if (mLocale != null && mLocale.toString().length() > 1) { reloadDictionaryIfRequired(); } } @Override public void close() { - if (!ExpandableBinaryDictionary.ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) { - closeBinaryDictionary(); + if (DBG_DUMP_ON_CLOSE) { + dumpAllWordsForDebug(); } // Flush pending writes. - // TODO: Remove after this class become to use a dynamic binary dictionary. - asyncFlashAllBinaryDictionary(); - Settings.writeLastUserHistoryWriteTime(mPrefs, mLocale); + flush(); + super.close(); + } + + public void flush() { + asyncFlushBinaryDictionary(); } @Override protected Map<String, String> getHeaderAttributeMap() { - HashMap<String, String> attributeMap = new HashMap<String, String>(); - attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE, - FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); - attributeMap.put(FormatSpec.FileHeader.USES_FORGETTING_CURVE_ATTRIBUTE, - FormatSpec.FileHeader.ATTRIBUTE_VALUE_TRUE); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_ID_ATTRIBUTE, mFileName); - attributeMap.put(FormatSpec.FileHeader.DICTIONARY_LOCALE_ATTRIBUTE, mLocale); + final Map<String, String> attributeMap = super.getHeaderAttributeMap(); + if (mAdditionalAttributeMap != null) { + attributeMap.putAll(mAdditionalAttributeMap); + } + attributeMap.put(DictionaryHeader.USES_FORGETTING_CURVE_KEY, + DictionaryHeader.ATTRIBUTE_VALUE_TRUE); + attributeMap.put(DictionaryHeader.HAS_HISTORICAL_INFO_KEY, + DictionaryHeader.ATTRIBUTE_VALUE_TRUE); return attributeMap; } @Override - protected boolean hasContentChanged() { + protected boolean haveContentsChanged() { return false; } - @Override - protected boolean needsToReloadBeforeWriting() { - return false; + public void addMultipleDictionaryEntriesToDictionary( + final ArrayList<LanguageModelParam> languageModelParams, + final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) { + if (languageModelParams == null || languageModelParams.isEmpty()) { + if (callback != null) { + callback.onFinished(); + } + return; + } + addMultipleDictionaryEntriesDynamically(languageModelParams, callback); } /** @@ -121,104 +111,28 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB * context, as in beginning of a sentence for example. * The second word may not be null (a NullPointerException would be thrown). */ - public void addToDictionary(final String word0, final String word1, final boolean isValid) { + public void addToDictionary(final String word0, final String word1, final boolean isValid, + final int timestamp) { if (word1.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH || (word0 != null && word0.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH)) { return; } - final int frequency = ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE ? - (isValid ? FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS) : - FREQUENCY_FOR_TYPED; - addWordDynamically(word1, null /* shortcutTarget */, frequency, 0 /* shortcutFreq */, - false /* isNotAWord */); + final int frequency = isValid ? + FREQUENCY_FOR_WORDS_IN_DICTS : FREQUENCY_FOR_WORDS_NOT_IN_DICTS; + addWordDynamically(word1, frequency, null /* shortcutTarget */, 0 /* shortcutFreq */, + false /* isNotAWord */, false /* isBlacklisted */, timestamp); // Do not insert a word as a bigram of itself if (word1.equals(word0)) { return; } if (null != word0) { - addBigramDynamically(word0, word1, frequency, isValid); + addBigramDynamically(word0, word1, frequency, timestamp); } } - public void cancelAddingUserHistory(final String word0, final String word1) { - removeBigramDynamically(word0, word1); - } - @Override - protected void loadDictionaryAsync() { - final int[] profTotalCount = { 0 }; - final String locale = getLocale(); - if (DBG_STRESS_TEST) { - try { - Log.w(TAG, "Start stress in loading: " + locale); - Thread.sleep(15000); - Log.w(TAG, "End stress in loading"); - } catch (InterruptedException e) { - } - } - final long last = Settings.readLastUserHistoryWriteTime(mPrefs, locale); - final long now = System.currentTimeMillis(); - final ExpandableBinaryDictionary dictionary = this; - final OnAddWordListener listener = new OnAddWordListener() { - @Override - public void setUnigram(final String word, final String shortcutTarget, - final int frequency, final int shortcutFreq) { - if (DBG_SAVE_RESTORE) { - Log.d(TAG, "load unigram: " + word + "," + frequency); - } - addWord(word, shortcutTarget, frequency, shortcutFreq, false /* isNotAWord */); - ++profTotalCount[0]; - } - - @Override - public void setBigram(final String word0, final String word1, final int frequency) { - if (word0.length() < Constants.DICTIONARY_MAX_WORD_LENGTH - && word1.length() < Constants.DICTIONARY_MAX_WORD_LENGTH) { - if (DBG_SAVE_RESTORE) { - Log.d(TAG, "load bigram: " + word0 + "," + word1 + "," + frequency); - } - ++profTotalCount[0]; - addBigram(word0, word1, frequency, last); - } - } - }; - - // Load the dictionary from binary file - final File dictFile = new File(mContext.getFilesDir(), mFileName); - final DictDecoder dictDecoder = FormatSpec.getDictDecoder(dictFile, - DictDecoder.USE_BYTEARRAY); - if (dictDecoder == null) { - // This is an expected condition: we don't have a user history dictionary for this - // language yet. It will be created sometime later. - return; - } - - try { - dictDecoder.openDictBuffer(); - UserHistoryDictIOUtils.readDictionaryBinary(dictDecoder, listener); - } catch (IOException e) { - Log.d(TAG, "IOException on opening a bytebuffer", e); - } finally { - if (PROFILE_SAVE_RESTORE) { - final long diff = System.currentTimeMillis() - now; - Log.d(TAG, "PROF: Load UserHistoryDictionary: " - + locale + ", " + diff + "ms. load " + profTotalCount[0] + "entries."); - } - } - } - - protected String getLocale() { - return mLocale; - } - - public void registerUpdateSession(PersonalizationDictionaryUpdateSession session) { - session.setPredictionDictionary(this); - mSessions.add(session); - session.onDictionaryReady(); - } - - public void unRegisterUpdateSession(PersonalizationDictionaryUpdateSession session) { - mSessions.remove(session); + protected void loadInitialContentsLocked() { + // No initial contents. } @UsedForTesting @@ -226,10 +140,17 @@ public abstract class DecayingExpandableBinaryDictionaryBase extends ExpandableB // Clear the node structure on memory clear(); // Then flush the cleared state of the dictionary on disk. - asyncFlashAllBinaryDictionary(); + asyncFlushBinaryDictionary(); + } + + @UsedForTesting + public void clearAndFlushDictionaryWithAdditionalAttributes( + final Map<String, String> attributeMap) { + mAdditionalAttributeMap = attributeMap; + clearAndFlushDictionary(); } - /* package */ void decayIfNeeded() { + /* package */ void runGCIfRequired() { runGCIfRequired(false /* mindsBlockByGC */); } } diff --git a/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java b/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java index e9ca662e7..de2744f29 100644 --- a/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java +++ b/java/src/com/android/inputmethod/latin/personalization/DictionaryDecayBroadcastReciever.java @@ -43,7 +43,7 @@ public class DictionaryDecayBroadcastReciever extends BroadcastReceiver { /** * Interval to update for decaying dictionaries. */ - private static final long DICTIONARY_DECAY_INTERVAL = TimeUnit.MINUTES.toMillis(60); + /* package */ static final long DICTIONARY_DECAY_INTERVAL = TimeUnit.MINUTES.toMillis(60); public static void setUpIntervalAlarmForDictionaryDecaying(Context context) { AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); @@ -60,7 +60,7 @@ public class DictionaryDecayBroadcastReciever extends BroadcastReceiver { public void onReceive(final Context context, final Intent intent) { final String action = intent.getAction(); if (action.equals(DICTIONARY_DECAY_INTENT_ACTION)) { - PersonalizationHelper.tryDecayingAllOpeningUserHistoryDictionary(); + PersonalizationHelper.runGCOnAllOpenedUserHistoryDictionaries(); } } } diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java deleted file mode 100644 index 6f152bb91..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.personalization; - -import android.content.Context; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.compat.ActivityManagerCompatUtils; -import com.android.inputmethod.keyboard.ProximityInfo; -import com.android.inputmethod.latin.AbstractDictionaryWriter; -import com.android.inputmethod.latin.ExpandableDictionary; -import com.android.inputmethod.latin.WordComposer; -import com.android.inputmethod.latin.ExpandableDictionary.NextWord; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.makedict.DictEncoder; -import com.android.inputmethod.latin.makedict.FormatSpec; -import com.android.inputmethod.latin.makedict.UnsupportedFormatException; -import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils; -import com.android.inputmethod.latin.utils.UserHistoryDictIOUtils.BigramDictionaryInterface; -import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils; -import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils.ForgettingCurveParams; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Map; - -// Currently this class is used to implement dynamic prodiction dictionary. -// TODO: Move to native code. -public class DynamicPersonalizationDictionaryWriter extends AbstractDictionaryWriter { - private static final String TAG = DynamicPersonalizationDictionaryWriter.class.getSimpleName(); - /** Maximum number of pairs. Pruning will start when databases goes above this number. */ - public static final int DEFAULT_MAX_HISTORY_BIGRAMS = 10000; - public static final int LOW_MEMORY_MAX_HISTORY_BIGRAMS = 2000; - - /** Any pair being typed or picked */ - private static final int FREQUENCY_FOR_TYPED = 2; - - private static final int BINARY_DICT_VERSION = 3; - private static final FormatSpec.FormatOptions FORMAT_OPTIONS = - new FormatSpec.FormatOptions(BINARY_DICT_VERSION, true /* supportsDynamicUpdate */); - - private final UserHistoryDictionaryBigramList mBigramList = - new UserHistoryDictionaryBigramList(); - private final ExpandableDictionary mExpandableDictionary; - private final int mMaxHistoryBigrams; - - public DynamicPersonalizationDictionaryWriter(final Context context, final String dictType) { - super(context, dictType); - mExpandableDictionary = new ExpandableDictionary(dictType); - final boolean isLowRamDevice = ActivityManagerCompatUtils.isLowRamDevice(context); - mMaxHistoryBigrams = isLowRamDevice ? - LOW_MEMORY_MAX_HISTORY_BIGRAMS : DEFAULT_MAX_HISTORY_BIGRAMS; - } - - @Override - public void clear() { - mBigramList.evictAll(); - mExpandableDictionary.clearDictionary(); - } - - /** - * Adds a word unigram to the fusion dictionary. Call updateBinaryDictionary when all changes - * are done to update the binary dictionary. - * @param word The word to add. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this unigram. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). Ignored - * if shortcutTarget is null. - * @param isNotAWord true if this is not a word, i.e. shortcut only. - */ - @Override - public void addUnigramWord(final String word, final String shortcutTarget, final int frequency, - final int shortcutFreq, final boolean isNotAWord) { - if (mBigramList.size() > mMaxHistoryBigrams * 2) { - // Too many entries: just stop adding new vocabulary and wait next refresh. - return; - } - mExpandableDictionary.addWord(word, shortcutTarget, frequency, shortcutFreq); - mBigramList.addBigram(null, word, (byte)frequency); - } - - @Override - public void addBigramWords(final String word0, final String word1, final int frequency, - final boolean isValid, final long lastModifiedTime) { - if (mBigramList.size() > mMaxHistoryBigrams * 2) { - // Too many entries: just stop adding new vocabulary and wait next refresh. - return; - } - if (lastModifiedTime > 0) { - mExpandableDictionary.setBigramAndGetFrequency(word0, word1, - new ForgettingCurveParams(frequency, System.currentTimeMillis(), - lastModifiedTime)); - mBigramList.addBigram(word0, word1, (byte)frequency); - } else { - mExpandableDictionary.setBigramAndGetFrequency(word0, word1, - new ForgettingCurveParams(isValid)); - mBigramList.addBigram(word0, word1, (byte)frequency); - } - } - - @Override - public void removeBigramWords(final String word0, final String word1) { - if (mBigramList.removeBigram(word0, word1)) { - mExpandableDictionary.removeBigram(word0, word1); - } - } - - @Override - protected void writeDictionary(final DictEncoder dictEncoder, - final Map<String, String> attributeMap) throws IOException, UnsupportedFormatException { - UserHistoryDictIOUtils.writeDictionary(dictEncoder, - new FrequencyProvider(mBigramList, mExpandableDictionary, mMaxHistoryBigrams), - mBigramList, FORMAT_OPTIONS); - } - - private static class FrequencyProvider implements BigramDictionaryInterface { - private final UserHistoryDictionaryBigramList mBigramList; - private final ExpandableDictionary mExpandableDictionary; - private final int mMaxHistoryBigrams; - - public FrequencyProvider(final UserHistoryDictionaryBigramList bigramList, - final ExpandableDictionary expandableDictionary, final int maxHistoryBigrams) { - mBigramList = bigramList; - mExpandableDictionary = expandableDictionary; - mMaxHistoryBigrams = maxHistoryBigrams; - } - - @Override - public int getFrequency(final String word0, final String word1) { - final int freq; - if (word0 == null) { // unigram - freq = FREQUENCY_FOR_TYPED; - } else { // bigram - final NextWord nw = mExpandableDictionary.getBigramWord(word0, word1); - if (nw != null) { - final ForgettingCurveParams forgettingCurveParams = nw.getFcParams(); - final byte prevFc = mBigramList.getBigrams(word0).get(word1); - final byte fc = forgettingCurveParams.getFc(); - final boolean isValid = forgettingCurveParams.isValid(); - if (prevFc > 0 && prevFc == fc) { - freq = fc & 0xFF; - } else if (UserHistoryForgettingCurveUtils. - needsToSave(fc, isValid, mBigramList.size() <= mMaxHistoryBigrams)) { - freq = fc & 0xFF; - } else { - // Delete this entry - freq = -1; - } - } else { - // Delete this entry - freq = -1; - } - } - return freq; - } - } - - @Override - public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, - final String prevWord, final ProximityInfo proximityInfo, - boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { - return mExpandableDictionary.getSuggestions(composer, prevWord, proximityInfo, - blockOffensiveWords, additionalFeaturesOptions); - } - - @Override - public boolean isValidWord(final String word) { - return mExpandableDictionary.isValidWord(word); - } - - @UsedForTesting - public boolean isInBigramListForTests(final String word) { - // TODO: Use native method to determine whether the word is in dictionary or not - return mBigramList.containsKey(word) || mBigramList.getBigrams(null).containsKey(word); - } -} diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java index f257165cb..4afd5b4c9 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java @@ -16,58 +16,29 @@ package com.android.inputmethod.latin.personalization; -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.ExpandableBinaryDictionary; -import com.android.inputmethod.latin.utils.CollectionUtils; - import android.content.Context; -import android.content.SharedPreferences; -import java.util.ArrayList; +import com.android.inputmethod.latin.Dictionary; -/** - * This class is a dictionary for the personalized language model that uses binary dictionary. - */ -public class PersonalizationDictionary extends ExpandableBinaryDictionary { - private static final String NAME = "personalization"; - private final ArrayList<PersonalizationDictionaryUpdateSession> mSessions = - CollectionUtils.newArrayList(); +import java.io.File; +import java.util.Locale; - /** Locale for which this user history dictionary is storing words */ - private final String mLocale; +public class PersonalizationDictionary extends DecayingExpandableBinaryDictionaryBase { + /* package */ static final String NAME = PersonalizationDictionary.class.getSimpleName(); - public PersonalizationDictionary(final Context context, final String locale, - final SharedPreferences prefs) { - // TODO: Make isUpdatable true. - super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_PERSONALIZATION, - false /* isUpdatable */); - mLocale = locale; - // TODO: Restore last updated time - loadDictionary(); + /* package */ PersonalizationDictionary(final Context context, final Locale locale) { + this(context, locale, null /* dictFile */); } - @Override - protected void loadDictionaryAsync() { - // TODO: Implement + public PersonalizationDictionary(final Context context, final Locale locale, + final File dictFile) { + super(context, getDictName(NAME, locale, dictFile), locale, Dictionary.TYPE_PERSONALIZATION, + dictFile); } @Override - protected boolean hasContentChanged() { + public boolean isValidWord(final String word) { + // Strings out of this dictionary should not be considered existing words. return false; } - - @Override - protected boolean needsToReloadBeforeWriting() { - return false; - } - - public void registerUpdateSession(PersonalizationDictionaryUpdateSession session) { - session.setDictionary(this); - mSessions.add(session); - session.onDictionaryReady(); - } - - public void unRegisterUpdateSession(PersonalizationDictionaryUpdateSession session) { - mSessions.remove(session); - } } diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegistrar.java index c1833ff14..d6c0dc0dc 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegistrar.java @@ -19,19 +19,26 @@ package com.android.inputmethod.latin.personalization; import android.content.Context; import android.content.res.Configuration; -public class PersonalizationDictionarySessionRegister { - public static void init(Context context) { +import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest; + +public class PersonalizationDictionarySessionRegistrar { + public static void init(final Context context, + final DictionaryFacilitatorForSuggest dictionaryFacilitator) { + } + + public static void onConfigurationChanged(final Context context, final Configuration conf, + final DictionaryFacilitatorForSuggest dictionaryFacilitator) { } - public static void onConfigurationChanged(final Context context, final Configuration conf) { + public static void onUpdateData(final Context context, final String type) { } - public static void onUpdateData(Context context, String type) { + public static void onRemoveData(final Context context, final String type) { } - public static void onRemoveData(Context context, String type) { + public static void resetAll(final Context context) { } - public static void onDestroy(Context context) { + public static void close(final Context context) { } } diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java deleted file mode 100644 index a86f6e584..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.personalization; - -import android.content.Context; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; - -/** - * This class is a session where a data provider can communicate with a personalization - * dictionary. - */ -public abstract class PersonalizationDictionaryUpdateSession { - /** - * This class is a parameter for a new unigram or bigram word which will be added - * to the personalization dictionary. - */ - public static class PersonalizationLanguageModelParam { - public final String mWord0; - public final String mWord1; - public final boolean mIsValid; - public final int mFrequency; - public PersonalizationLanguageModelParam(String word0, String word1, boolean isValid, - int frequency) { - mWord0 = word0; - mWord1 = word1; - mIsValid = isValid; - mFrequency = frequency; - } - } - - // TODO: Use a dynamic binary dictionary instead - public WeakReference<PersonalizationDictionary> mDictionary; - public WeakReference<DecayingExpandableBinaryDictionaryBase> mPredictionDictionary; - public final String mSystemLocale; - public PersonalizationDictionaryUpdateSession(String locale) { - mSystemLocale = locale; - } - - public abstract void onDictionaryReady(); - - public abstract void onDictionaryClosed(Context context); - - public void setDictionary(PersonalizationDictionary dictionary) { - mDictionary = new WeakReference<PersonalizationDictionary>(dictionary); - } - - public void setPredictionDictionary(DecayingExpandableBinaryDictionaryBase dictionary) { - mPredictionDictionary = - new WeakReference<DecayingExpandableBinaryDictionaryBase>(dictionary); - } - - protected PersonalizationDictionary getDictionary() { - return mDictionary == null ? null : mDictionary.get(); - } - - protected DecayingExpandableBinaryDictionaryBase getPredictionDictionary() { - return mPredictionDictionary == null ? null : mPredictionDictionary.get(); - } - - private void unsetDictionary() { - final PersonalizationDictionary dictionary = getDictionary(); - if (dictionary == null) { - return; - } - dictionary.unRegisterUpdateSession(this); - } - - private void unsetPredictionDictionary() { - final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary(); - if (dictionary == null) { - return; - } - dictionary.unRegisterUpdateSession(this); - } - - public void clearAndFlushPredictionDictionary(Context context) { - final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary(); - if (dictionary == null) { - return; - } - dictionary.clearAndFlushDictionary(); - } - - public void closeSession(Context context) { - unsetDictionary(); - unsetPredictionDictionary(); - onDictionaryClosed(context); - } - - // TODO: Support multi locale to add bigram - public void addBigramToPersonalizationDictionary(String word0, String word1, boolean isValid, - int frequency) { - final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary(); - if (dictionary == null) { - return; - } - dictionary.addToDictionary(word0, word1, isValid); - } - - // Bulk import - // TODO: Support multi locale to add bigram - public void addBigramsToPersonalizationDictionary( - final ArrayList<PersonalizationLanguageModelParam> lmParams) { - final DecayingExpandableBinaryDictionaryBase dictionary = getPredictionDictionary(); - if (dictionary == null) { - return; - } - for (final PersonalizationLanguageModelParam lmParam : lmParams) { - dictionary.addToDictionary(lmParam.mWord0, lmParam.mWord1, lmParam.mIsValid); - } - } -} diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java index 221ddeeba..385b525b6 100644 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java +++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java @@ -16,36 +16,35 @@ package com.android.inputmethod.latin.personalization; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.FileUtils; import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; import android.util.Log; +import java.io.File; +import java.io.FilenameFilter; import java.lang.ref.SoftReference; +import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; public class PersonalizationHelper { private static final String TAG = PersonalizationHelper.class.getSimpleName(); private static final boolean DEBUG = false; private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>> sLangUserHistoryDictCache = CollectionUtils.newConcurrentHashMap(); - private static final ConcurrentHashMap<String, SoftReference<PersonalizationDictionary>> sLangPersonalizationDictCache = CollectionUtils.newConcurrentHashMap(); - private static final ConcurrentHashMap<String, - SoftReference<PersonalizationPredictionDictionary>> - sLangPersonalizationPredictionDictCache = - CollectionUtils.newConcurrentHashMap(); - public static UserHistoryDictionary getUserHistoryDictionary( - final Context context, final String locale, final SharedPreferences sp) { + final Context context, final Locale locale) { + final String localeStr = locale.toString(); synchronized (sLangUserHistoryDictCache) { - if (sLangUserHistoryDictCache.containsKey(locale)) { + if (sLangUserHistoryDictCache.containsKey(localeStr)) { final SoftReference<UserHistoryDictionary> ref = - sLangUserHistoryDictCache.get(locale); + sLangUserHistoryDictCache.get(localeStr); final UserHistoryDictionary dict = ref == null ? null : ref.get(); if (dict != null) { if (DEBUG) { @@ -55,77 +54,111 @@ public class PersonalizationHelper { return dict; } } - final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale, sp); - sLangUserHistoryDictCache.put(locale, new SoftReference<UserHistoryDictionary>(dict)); + final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale); + sLangUserHistoryDictCache.put(localeStr, + new SoftReference<UserHistoryDictionary>(dict)); return dict; } } - public static void tryDecayingAllOpeningUserHistoryDictionary() { - for (final ConcurrentHashMap.Entry<String, SoftReference<UserHistoryDictionary>> entry - : sLangUserHistoryDictCache.entrySet()) { - if (entry.getValue() != null) { - final UserHistoryDictionary dict = entry.getValue().get(); - if (dict != null) { - dict.decayIfNeeded(); - } - } + private static int sCurrentTimestampForTesting = 0; + public static void currentTimeChangedForTesting(final int currentTimestamp) { + if (TimeUnit.MILLISECONDS.toSeconds( + DictionaryDecayBroadcastReciever.DICTIONARY_DECAY_INTERVAL) + < currentTimestamp - sCurrentTimestampForTesting) { + // TODO: Run GC for both PersonalizationDictionary and UserHistoryDictionary. + runGCOnAllOpenedUserHistoryDictionaries(); } } - public static void registerPersonalizationDictionaryUpdateSession(final Context context, - final PersonalizationDictionaryUpdateSession session, String locale) { - final PersonalizationPredictionDictionary predictionDictionary = - getPersonalizationPredictionDictionary(context, locale, - PreferenceManager.getDefaultSharedPreferences(context)); - predictionDictionary.registerUpdateSession(session); - final PersonalizationDictionary dictionary = - getPersonalizationDictionary(context, locale, - PreferenceManager.getDefaultSharedPreferences(context)); - dictionary.registerUpdateSession(session); + public static void runGCOnAllOpenedUserHistoryDictionaries() { + runGCOnAllDictionariesIfRequired(sLangUserHistoryDictCache); + } + + @UsedForTesting + public static void runGCOnAllOpenedPersonalizationDictionaries() { + runGCOnAllDictionariesIfRequired(sLangPersonalizationDictCache); + } + + private static <T extends DecayingExpandableBinaryDictionaryBase> + void runGCOnAllDictionariesIfRequired( + final ConcurrentHashMap<String, SoftReference<T>> dictionaryMap) { + for (final ConcurrentHashMap.Entry<String, SoftReference<T>> entry + : dictionaryMap.entrySet()) { + final DecayingExpandableBinaryDictionaryBase dict = entry.getValue().get(); + if (dict != null) { + dict.runGCIfRequired(); + } else { + dictionaryMap.remove(entry.getKey()); + } + } } public static PersonalizationDictionary getPersonalizationDictionary( - final Context context, final String locale, final SharedPreferences sp) { + final Context context, final Locale locale) { + final String localeStr = locale.toString(); synchronized (sLangPersonalizationDictCache) { - if (sLangPersonalizationDictCache.containsKey(locale)) { + if (sLangPersonalizationDictCache.containsKey(localeStr)) { final SoftReference<PersonalizationDictionary> ref = - sLangPersonalizationDictCache.get(locale); + sLangPersonalizationDictCache.get(localeStr); final PersonalizationDictionary dict = ref == null ? null : ref.get(); if (dict != null) { if (DEBUG) { - Log.w(TAG, "Use cached PersonalizationDictCache for " + locale); + Log.w(TAG, "Use cached PersonalizationDictionary for " + locale); } return dict; } } - final PersonalizationDictionary dict = - new PersonalizationDictionary(context, locale, sp); + final PersonalizationDictionary dict = new PersonalizationDictionary(context, locale); sLangPersonalizationDictCache.put( - locale, new SoftReference<PersonalizationDictionary>(dict)); + localeStr, new SoftReference<PersonalizationDictionary>(dict)); return dict; } } - public static PersonalizationPredictionDictionary getPersonalizationPredictionDictionary( - final Context context, final String locale, final SharedPreferences sp) { - synchronized (sLangPersonalizationPredictionDictCache) { - if (sLangPersonalizationPredictionDictCache.containsKey(locale)) { - final SoftReference<PersonalizationPredictionDictionary> ref = - sLangPersonalizationPredictionDictCache.get(locale); - final PersonalizationPredictionDictionary dict = ref == null ? null : ref.get(); - if (dict != null) { - if (DEBUG) { - Log.w(TAG, "Use cached PersonalizationPredictionDictionary for " + locale); + public static void removeAllPersonalizationDictionaries(final Context context) { + removeAllDictionaries(context, sLangPersonalizationDictCache, + PersonalizationDictionary.NAME); + } + + public static void removeAllUserHistoryDictionaries(final Context context) { + removeAllDictionaries(context, sLangUserHistoryDictCache, + UserHistoryDictionary.NAME); + } + + private static <T extends DecayingExpandableBinaryDictionaryBase> void removeAllDictionaries( + final Context context, final ConcurrentHashMap<String, SoftReference<T>> dictionaryMap, + final String dictNamePrefix) { + synchronized (dictionaryMap) { + for (final ConcurrentHashMap.Entry<String, SoftReference<T>> entry + : dictionaryMap.entrySet()) { + if (entry.getValue() != null) { + final DecayingExpandableBinaryDictionaryBase dict = entry.getValue().get(); + if (dict != null) { + dict.clearAndFlushDictionary(); } - return dict; } } - final PersonalizationPredictionDictionary dict = - new PersonalizationPredictionDictionary(context, locale, sp); - sLangPersonalizationPredictionDictCache.put( - locale, new SoftReference<PersonalizationPredictionDictionary>(dict)); - return dict; + dictionaryMap.clear(); + if (!FileUtils.deleteFilteredFiles( + context.getFilesDir(), new DictFilter(dictNamePrefix))) { + Log.e(TAG, "Cannot remove all existing dictionary files. filesDir: " + + context.getFilesDir().getAbsolutePath() + ", dictNamePrefix: " + + dictNamePrefix); + } + } + } + + private static class DictFilter implements FilenameFilter { + private final String mName; + + DictFilter(final String name) { + mName = name; + } + + @Override + public boolean accept(final File dir, final String name) { + return name.startsWith(mName); } } } diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java deleted file mode 100644 index 432954453..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.personalization; - -import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.ExpandableBinaryDictionary; - -import android.content.Context; -import android.content.SharedPreferences; - -public class PersonalizationPredictionDictionary extends DecayingExpandableBinaryDictionaryBase { - private static final String NAME = PersonalizationPredictionDictionary.class.getSimpleName(); - - /* package */ PersonalizationPredictionDictionary(final Context context, final String locale, - final SharedPreferences sp) { - super(context, locale, sp, Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA, - getDictionaryFileName(locale)); - } - - private static String getDictionaryFileName(final String locale) { - return NAME + "." + locale + ExpandableBinaryDictionary.DICT_FILE_EXTENSION; - } -} diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java index a60226d7e..504e9b2f3 100644 --- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java +++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java @@ -16,25 +16,37 @@ package com.android.inputmethod.latin.personalization; +import android.content.Context; + import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.ExpandableBinaryDictionary; -import android.content.Context; -import android.content.SharedPreferences; +import java.io.File; +import java.util.Locale; /** * Locally gathers stats about the words user types and various other signals like auto-correction * cancellation or manual picks. This allows the keyboard to adapt to the typist over time. */ public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBase { - /* package for tests */ static final String NAME = - UserHistoryDictionary.class.getSimpleName(); - /* package */ UserHistoryDictionary(final Context context, final String locale, - final SharedPreferences sp) { - super(context, locale, sp, Dictionary.TYPE_USER_HISTORY, getDictionaryFileName(locale)); + /* package */ static final String NAME = UserHistoryDictionary.class.getSimpleName(); + + /* package */ UserHistoryDictionary(final Context context, final Locale locale) { + this(context, locale, null /* dictFile */); + } + + public UserHistoryDictionary(final Context context, final Locale locale, + final File dictFile) { + super(context, getDictName(NAME, locale, dictFile), locale, Dictionary.TYPE_USER_HISTORY, + dictFile); + } + + public void cancelAddingUserHistory(final String word0, final String word1) { + removeBigramDynamically(word0, word1); } - private static String getDictionaryFileName(final String locale) { - return NAME + "." + locale + ExpandableBinaryDictionary.DICT_FILE_EXTENSION; + @Override + public boolean isValidWord(final String word) { + // Strings out of this dictionary should not be considered existing words. + return false; } } diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java deleted file mode 100644 index 55a90ee51..000000000 --- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2012 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.personalization; - -import android.util.Log; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.utils.CollectionUtils; - -import java.util.HashMap; -import java.util.Set; - -/** - * A store of bigrams which will be updated when the user history dictionary is closed - * All bigrams including stale ones in SQL DB should be stored in this class to avoid adding stale - * bigrams when we write to the SQL DB. - */ -@UsedForTesting -public final class UserHistoryDictionaryBigramList { - public static final byte FORGETTING_CURVE_INITIAL_VALUE = 0; - private static final String TAG = UserHistoryDictionaryBigramList.class.getSimpleName(); - private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = CollectionUtils.newHashMap(); - private final HashMap<String, HashMap<String, Byte>> mBigramMap = CollectionUtils.newHashMap(); - private int mSize = 0; - - public void evictAll() { - mSize = 0; - mBigramMap.clear(); - } - - /** - * Called when the user typed a word. - */ - @UsedForTesting - public void addBigram(String word1, String word2) { - addBigram(word1, word2, FORGETTING_CURVE_INITIAL_VALUE); - } - - /** - * Called when loaded from the SQL DB. - */ - public void addBigram(String word1, String word2, byte fcValue) { - if (DecayingExpandableBinaryDictionaryBase.DBG_SAVE_RESTORE) { - Log.d(TAG, "--- add bigram: " + word1 + ", " + word2 + ", " + fcValue); - } - final HashMap<String, Byte> map; - if (mBigramMap.containsKey(word1)) { - map = mBigramMap.get(word1); - } else { - map = CollectionUtils.newHashMap(); - mBigramMap.put(word1, map); - } - if (!map.containsKey(word2)) { - ++mSize; - map.put(word2, fcValue); - } - } - - /** - * Called when inserted to the SQL DB. - */ - public void updateBigram(String word1, String word2, byte fcValue) { - if (DecayingExpandableBinaryDictionaryBase.DBG_SAVE_RESTORE) { - Log.d(TAG, "--- update bigram: " + word1 + ", " + word2 + ", " + fcValue); - } - final HashMap<String, Byte> map; - if (mBigramMap.containsKey(word1)) { - map = mBigramMap.get(word1); - } else { - return; - } - if (!map.containsKey(word2)) { - return; - } - map.put(word2, fcValue); - } - - public int size() { - return mSize; - } - - public boolean isEmpty() { - return mBigramMap.isEmpty(); - } - - public boolean containsKey(String word) { - return mBigramMap.containsKey(word); - } - - public Set<String> keySet() { - return mBigramMap.keySet(); - } - - public HashMap<String, Byte> getBigrams(String word1) { - if (mBigramMap.containsKey(word1)) return mBigramMap.get(word1); - // TODO: lower case according to locale - final String lowerWord1 = word1.toLowerCase(); - if (mBigramMap.containsKey(lowerWord1)) return mBigramMap.get(lowerWord1); - return EMPTY_BIGRAM_MAP; - } - - public boolean removeBigram(String word1, String word2) { - final HashMap<String, Byte> set = getBigrams(word1); - if (set.isEmpty()) { - return false; - } - if (set.containsKey(word2)) { - set.remove(word2); - --mSize; - return true; - } - return false; - } -} diff --git a/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java index 4bf524cbb..39977e76f 100644 --- a/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java +++ b/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java @@ -16,8 +16,6 @@ package com.android.inputmethod.latin.settings; -import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE; - import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; @@ -44,10 +42,13 @@ import android.widget.Spinner; import android.widget.SpinnerAdapter; import android.widget.Toast; +import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.DialogUtils; import com.android.inputmethod.latin.utils.IntentUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; @@ -111,7 +112,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment { subtype.getLocale(), subtype.hashCode(), subtype.hashCode(), SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype))); } - if (subtype.containsExtraValueKey(ASCII_CAPABLE)) { + if (InputMethodSubtypeCompatUtils.isAsciiCapable(subtype)) { items.add(createItem(context, subtype.getLocale())); } } @@ -287,7 +288,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment { final KeyboardLayoutSetItem layout = (KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem(); final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype( - locale.first, layout.first, ASCII_CAPABLE); + locale.first, layout.first, Constants.Subtype.ExtraValue.ASCII_CAPABLE); setSubtype(subtype); notifyChanged(); if (isEditing) { @@ -517,7 +518,8 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment { private AlertDialog createDialog( @SuppressWarnings("unused") final SubtypePreference subtypePref) { - final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + final AlertDialog.Builder builder = new AlertDialog.Builder( + DialogUtils.getPlatformDialogThemeContext(getActivity())); builder.setTitle(R.string.custom_input_styles_title) .setMessage(R.string.custom_input_style_note_message) .setNegativeButton(R.string.not_now, null) diff --git a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java index da1fb73fe..11d369282 100644 --- a/java/src/com/android/inputmethod/latin/settings/DebugSettings.java +++ b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java @@ -16,19 +16,24 @@ package com.android.inputmethod.latin.settings; +import android.content.Intent; import android.content.SharedPreferences; +import android.content.res.Resources; import android.os.Bundle; import android.os.Process; import android.preference.CheckBoxPreference; import android.preference.Preference; +import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceFragment; import android.preference.PreferenceScreen; -import com.android.inputmethod.keyboard.KeyboardSwitcher; +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.DictionaryDumpBroadcastReceiver; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.debug.ExternalDictionaryGetterForDebug; import com.android.inputmethod.latin.utils.ApplicationUtils; +import com.android.inputmethod.latin.utils.ResourceUtils; public final class DebugSettings extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { @@ -37,11 +42,20 @@ public final class DebugSettings extends PreferenceFragment public static final String PREF_FORCE_NON_DISTINCT_MULTITOUCH = "force_non_distinct_multitouch"; public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; public static final String PREF_STATISTICS_LOGGING = "enable_logging"; - public static final String PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG = - "use_only_personalization_dictionary_for_debug"; - public static final String PREF_BOOST_PERSONALIZATION_DICTIONARY_FOR_DEBUG = - "boost_personalization_dictionary_for_debug"; + public static final String PREF_KEY_PREVIEW_SHOW_UP_START_SCALE = + "pref_key_preview_show_up_start_scale"; + public static final String PREF_KEY_PREVIEW_DISMISS_END_SCALE = + "pref_key_preview_dismiss_end_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"; private static final String PREF_READ_EXTERNAL_DICTIONARY = "read_external_dictionary"; + private static final String PREF_DUMP_CONTACTS_DICT = "dump_contacts_dict"; + private static final String PREF_DUMP_USER_DICT = "dump_user_dict"; + private static final String PREF_DUMP_USER_HISTORY_DICT = "dump_user_history_dict"; + private static final String PREF_DUMP_PERSONALIZATION_DICT = "dump_personalization_dict"; + private static final boolean SHOW_STATISTICS_LOGGING = false; private boolean mServiceNeedsRestart = false; @@ -85,11 +99,64 @@ public final class DebugSettings extends PreferenceFragment }); } + final OnPreferenceClickListener dictDumpPrefClickListener = + new DictDumpPrefClickListener(this); + findPreference(PREF_DUMP_CONTACTS_DICT).setOnPreferenceClickListener( + dictDumpPrefClickListener); + findPreference(PREF_DUMP_USER_DICT).setOnPreferenceClickListener( + dictDumpPrefClickListener); + findPreference(PREF_DUMP_USER_HISTORY_DICT).setOnPreferenceClickListener( + dictDumpPrefClickListener); + findPreference(PREF_DUMP_PERSONALIZATION_DICT).setOnPreferenceClickListener( + dictDumpPrefClickListener); + final Resources res = getResources(); + setupKeyPreviewAnimationDuration(prefs, res, PREF_KEY_PREVIEW_SHOW_UP_DURATION, + res.getInteger(R.integer.config_key_preview_show_up_duration)); + setupKeyPreviewAnimationDuration(prefs, res, PREF_KEY_PREVIEW_DISMISS_DURATION, + res.getInteger(R.integer.config_key_preview_dismiss_duration)); + setupKeyPreviewAnimationScale(prefs, res, PREF_KEY_PREVIEW_SHOW_UP_START_SCALE, + ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_show_up_start_scale)); + setupKeyPreviewAnimationScale(prefs, res, PREF_KEY_PREVIEW_DISMISS_END_SCALE, + ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_dismiss_end_scale)); + mServiceNeedsRestart = false; mDebugMode = (CheckBoxPreference) findPreference(PREF_DEBUG_MODE); updateDebugMode(); } + private static class DictDumpPrefClickListener implements OnPreferenceClickListener { + final PreferenceFragment mPreferenceFragment; + + public DictDumpPrefClickListener(final PreferenceFragment preferenceFragment) { + mPreferenceFragment = preferenceFragment; + } + + @Override + public boolean onPreferenceClick(final Preference arg0) { + final String dictName; + if (arg0.getKey().equals(PREF_DUMP_CONTACTS_DICT)) { + dictName = Dictionary.TYPE_CONTACTS; + } else if (arg0.getKey().equals(PREF_DUMP_USER_DICT)) { + dictName = Dictionary.TYPE_USER; + } else if (arg0.getKey().equals(PREF_DUMP_USER_HISTORY_DICT)) { + dictName = Dictionary.TYPE_USER_HISTORY; + } else if (arg0.getKey().equals(PREF_DUMP_PERSONALIZATION_DICT)) { + dictName = Dictionary.TYPE_PERSONALIZATION; + } else { + dictName = null; + } + if (dictName != null) { + final Intent intent = + new Intent(DictionaryDumpBroadcastReceiver.DICTIONARY_DUMP_INTENT_ACTION); + intent.putExtra(DictionaryDumpBroadcastReceiver.DICTIONARY_NAME_KEY, dictName); + mPreferenceFragment.getActivity().sendBroadcast(intent); + } + return true; + } + } + @Override public void onStop() { super.onStop(); @@ -112,8 +179,7 @@ public final class DebugSettings extends PreferenceFragment updateDebugMode(); mServiceNeedsRestart = true; } - } else if (key.equals(PREF_FORCE_NON_DISTINCT_MULTITOUCH) - || key.equals(PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG)) { + } else if (key.equals(PREF_FORCE_NON_DISTINCT_MULTITOUCH)) { mServiceNeedsRestart = true; } } @@ -133,4 +199,92 @@ public final class DebugSettings extends PreferenceFragment mDebugMode.setSummary(version); } } + + private void setupKeyPreviewAnimationScale(final SharedPreferences sp, final Resources res, + final String prefKey, final float defaultValue) { + final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey); + if (pref == null) { + return; + } + pref.setInterface(new SeekBarDialogPreference.ValueProxy() { + private static final float PERCENTAGE_FLOAT = 100.0f; + + private float getValueFromPercentage(final int percentage) { + return percentage / PERCENTAGE_FLOAT; + } + + private int getPercentageFromValue(final float floatValue) { + return (int)(floatValue * PERCENTAGE_FLOAT); + } + + @Override + public void writeValue(final int value, final String key) { + sp.edit().putFloat(key, getValueFromPercentage(value)).apply(); + } + + @Override + public void writeDefaultValue(final String key) { + sp.edit().remove(key).apply(); + } + + @Override + public int readValue(final String key) { + return getPercentageFromValue( + Settings.readKeyPreviewAnimationScale(sp, key, defaultValue)); + } + + @Override + public int readDefaultValue(final String key) { + return getPercentageFromValue(defaultValue); + } + + @Override + public String getValueText(final int value) { + if (value < 0) { + return res.getString(R.string.settings_system_default); + } + return String.format("%d%%", value); + } + + @Override + public void feedbackValue(final int value) {} + }); + } + + private void setupKeyPreviewAnimationDuration(final SharedPreferences sp, final Resources res, + final String prefKey, final int defaultValue) { + final SeekBarDialogPreference pref = (SeekBarDialogPreference)findPreference(prefKey); + if (pref == null) { + return; + } + pref.setInterface(new SeekBarDialogPreference.ValueProxy() { + @Override + public void writeValue(final int value, final String key) { + sp.edit().putInt(key, value).apply(); + } + + @Override + public void writeDefaultValue(final String key) { + sp.edit().remove(key).apply(); + } + + @Override + public int readValue(final String key) { + return Settings.readKeyPreviewAnimationDuration(sp, key, defaultValue); + } + + @Override + public int readDefaultValue(final String key) { + return defaultValue; + } + + @Override + public String getValueText(final int value) { + return res.getString(R.string.abbreviation_unit_milliseconds, value); + } + + @Override + public void feedbackValue(final int value) {} + }); + } } diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java index df2c6907f..353b7463d 100644 --- a/java/src/com/android/inputmethod/latin/settings/Settings.java +++ b/java/src/com/android/inputmethod/latin/settings/Settings.java @@ -27,13 +27,13 @@ import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; import com.android.inputmethod.latin.InputAttributes; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; -import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.ResourceUtils; import com.android.inputmethod.latin.utils.RunInLocale; import com.android.inputmethod.latin.utils.StringUtils; -import java.util.HashMap; +import java.util.Collections; import java.util.Locale; +import java.util.Set; import java.util.concurrent.locks.ReentrantLock; public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener { @@ -53,10 +53,9 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold"; public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting"; public static final String PREF_MISC_SETTINGS = "misc_settings"; - public static final String PREF_LAST_USER_DICTIONARY_WRITE_TIME = - "last_user_dictionary_write_time"; public static final String PREF_ADVANCED_SETTINGS = "pref_advanced_settings"; public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict"; + public static final String PREF_KEY_USE_PERSONALIZED_DICTS = "pref_key_use_personalized_dicts"; public static final String PREF_KEY_USE_DOUBLE_SPACE_PERIOD = "pref_key_use_double_space_period"; public static final String PREF_BLOCK_POTENTIALLY_OFFENSIVE = @@ -67,6 +66,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang "pref_include_other_imes_in_language_switch_list"; public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20110916"; public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles"; + // 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"; public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction"; @@ -96,6 +96,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang private static final String PREF_LAST_USED_PERSONALIZATION_TOKEN = "pref_last_used_personalization_token"; + private static final String PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME = + "pref_last_used_personalization_dict_wiped_time"; + private static final String PREF_CORPUS_HANDLES_FOR_PERSONALIZATION = + "pref_corpus_handles_for_personalization"; public static final String PREF_SEND_FEEDBACK = "send_feedback"; public static final String PREF_ABOUT_KEYBOARD = "about_keyboard"; @@ -104,6 +108,10 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_EMOJI_CATEGORY_LAST_TYPED_ID = "emoji_category_last_typed_id"; public static final String PREF_LAST_SHOWN_EMOJI_CATEGORY_ID = "last_shown_emoji_category_id"; + private static final float UNDEFINED_PREFERENCE_VALUE_FLOAT = -1.0f; + private static final int UNDEFINED_PREFERENCE_VALUE_INT = -1; + + private Context mContext; private Resources mRes; private SharedPreferences mPrefs; private SettingsValues mSettingsValues; @@ -124,6 +132,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang } private void onCreate(final Context context) { + mContext = context; mRes = context.getResources(); mPrefs = PreferenceManager.getDefaultSharedPreferences(context); mPrefs.registerOnSharedPreferenceChangeListener(this); @@ -143,20 +152,22 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang Log.w(TAG, "onSharedPreferenceChanged called before loadSettings."); return; } - loadSettings(mSettingsValues.mLocale, mSettingsValues.mInputAttributes); + loadSettings(mContext, mSettingsValues.mLocale, mSettingsValues.mInputAttributes); } finally { mSettingsValuesLock.unlock(); } } - public void loadSettings(final Locale locale, final InputAttributes inputAttributes) { + public void loadSettings(final Context context, final Locale locale, + final InputAttributes inputAttributes) { mSettingsValuesLock.lock(); + mContext = context; try { final SharedPreferences prefs = mPrefs; final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() { @Override protected SettingsValues job(final Resources res) { - return new SettingsValues(prefs, locale, res, inputAttributes); + return new SettingsValues(context, prefs, res, inputAttributes); } }; mSettingsValues = job.runInLocale(mRes, locale); @@ -174,10 +185,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return mSettingsValues.mIsInternal; } - public String getWordSeparators() { - return mSettingsValues.mWordSeparators; - } - public boolean isWordSeparator(final int code) { return mSettingsValues.isWordSeparator(code); } @@ -229,16 +236,15 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang res.getBoolean(R.bool.config_default_phrase_gesture_enabled)); } - public static boolean readFromBuildConfigIfToShowKeyPreviewPopupSettingsOption( - final Resources res) { - return res.getBoolean(R.bool.config_enable_show_option_of_key_preview_popup); + public static boolean readFromBuildConfigIfToShowKeyPreviewPopupOption(final Resources res) { + return res.getBoolean(R.bool.config_enable_show_key_preview_popup_option); } public static boolean readKeyPreviewPopupEnabled(final SharedPreferences prefs, final Resources res) { final boolean defaultKeyPreviewPopup = res.getBoolean( R.bool.config_default_key_preview_popup); - if (!readFromBuildConfigIfToShowKeyPreviewPopupSettingsOption(res)) { + if (!readFromBuildConfigIfToShowKeyPreviewPopupOption(res)) { return defaultKeyPreviewPopup; } return prefs.getBoolean(PREF_POPUP_ON, defaultKeyPreviewPopup); @@ -263,28 +269,6 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return prefs.getBoolean(PREF_SHOW_LANGUAGE_SWITCH_KEY, true); } - public static int readKeyboardThemeIndex(final SharedPreferences prefs, final Resources res) { - final String defaultThemeIndex = res.getString( - R.string.config_default_keyboard_theme_index); - final String themeIndex = prefs.getString(PREF_KEYBOARD_LAYOUT, defaultThemeIndex); - try { - return Integer.valueOf(themeIndex); - } catch (final NumberFormatException e) { - // Format error, returns default keyboard theme index. - Log.e(TAG, "Illegal keyboard theme in preference: " + themeIndex + ", default to " - + defaultThemeIndex, e); - return Integer.valueOf(defaultThemeIndex); - } - } - - public static int resetAndGetDefaultKeyboardThemeIndex(final SharedPreferences prefs, - final Resources res) { - final String defaultThemeIndex = res.getString( - R.string.config_default_keyboard_theme_index); - prefs.edit().putString(PREF_KEYBOARD_LAYOUT, defaultThemeIndex).apply(); - return Integer.valueOf(defaultThemeIndex); - } - public static String readPrefAdditionalSubtypes(final SharedPreferences prefs, final Resources res) { final String predefinedPrefSubtypes = AdditionalSubtypeUtils.createPrefSubtypes( @@ -299,19 +283,27 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static float readKeypressSoundVolume(final SharedPreferences prefs, final Resources res) { - final float volume = prefs.getFloat(PREF_KEYPRESS_SOUND_VOLUME, -1.0f); - return (volume >= 0) ? volume : readDefaultKeypressSoundVolume(res); + final float volume = prefs.getFloat( + PREF_KEYPRESS_SOUND_VOLUME, UNDEFINED_PREFERENCE_VALUE_FLOAT); + return (volume != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? volume + : readDefaultKeypressSoundVolume(res); } + // Default keypress sound volume for unknown devices. + // The negative value means system default. + private static final String DEFAULT_KEYPRESS_SOUND_VOLUME = Float.toString(-1.0f); + public static float readDefaultKeypressSoundVolume(final Resources res) { - return Float.parseFloat( - ResourceUtils.getDeviceOverrideValue(res, R.array.keypress_volumes)); + return Float.parseFloat(ResourceUtils.getDeviceOverrideValue(res, + R.array.keypress_volumes, DEFAULT_KEYPRESS_SOUND_VOLUME)); } public static int readKeyLongpressTimeout(final SharedPreferences prefs, final Resources res) { - final int ms = prefs.getInt(PREF_KEY_LONGPRESS_TIMEOUT, -1); - return (ms >= 0) ? ms : readDefaultKeyLongpressTimeout(res); + final int milliseconds = prefs.getInt( + PREF_KEY_LONGPRESS_TIMEOUT, UNDEFINED_PREFERENCE_VALUE_INT); + return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds + : readDefaultKeyLongpressTimeout(res); } public static int readDefaultKeyLongpressTimeout(final Resources res) { @@ -320,36 +312,35 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static int readKeypressVibrationDuration(final SharedPreferences prefs, final Resources res) { - final int ms = prefs.getInt(PREF_VIBRATION_DURATION_SETTINGS, -1); - return (ms >= 0) ? ms : readDefaultKeypressVibrationDuration(res); + final int milliseconds = prefs.getInt( + PREF_VIBRATION_DURATION_SETTINGS, UNDEFINED_PREFERENCE_VALUE_INT); + return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds + : readDefaultKeypressVibrationDuration(res); } + // Default keypress vibration duration for unknown devices. + // The negative value means system default. + private static final String DEFAULT_KEYPRESS_VIBRATION_DURATION = Integer.toString(-1); + public static int readDefaultKeypressVibrationDuration(final Resources res) { - return Integer.parseInt( - ResourceUtils.getDeviceOverrideValue(res, R.array.keypress_vibration_durations)); + return Integer.parseInt(ResourceUtils.getDeviceOverrideValue(res, + R.array.keypress_vibration_durations, DEFAULT_KEYPRESS_VIBRATION_DURATION)); } public static boolean readUsabilityStudyMode(final SharedPreferences prefs) { return prefs.getBoolean(DebugSettings.PREF_USABILITY_STUDY_MODE, true); } - public static long readLastUserHistoryWriteTime(final SharedPreferences prefs, - final String locale) { - final String str = prefs.getString(PREF_LAST_USER_DICTIONARY_WRITE_TIME, ""); - final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(str); - if (map.containsKey(locale)) { - return map.get(locale); - } - return 0; + public static float readKeyPreviewAnimationScale(final SharedPreferences prefs, + final String prefKey, final float defaultValue) { + final float fraction = prefs.getFloat(prefKey, UNDEFINED_PREFERENCE_VALUE_FLOAT); + return (fraction != UNDEFINED_PREFERENCE_VALUE_FLOAT) ? fraction : defaultValue; } - public static void writeLastUserHistoryWriteTime(final SharedPreferences prefs, - final String locale) { - final String oldStr = prefs.getString(PREF_LAST_USER_DICTIONARY_WRITE_TIME, ""); - final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(oldStr); - map.put(locale, System.currentTimeMillis()); - final String newStr = LocaleUtils.localeAndTimeHashMapToStr(map); - prefs.edit().putString(PREF_LAST_USER_DICTIONARY_WRITE_TIME, newStr).apply(); + public static int readKeyPreviewAnimationDuration(final SharedPreferences prefs, + final String prefKey, final int defaultValue) { + final int milliseconds = prefs.getInt(prefKey, UNDEFINED_PREFERENCE_VALUE_INT); + return (milliseconds != UNDEFINED_PREFERENCE_VALUE_INT) ? milliseconds : defaultValue; } public static boolean readUseFullscreenMode(final Resources res) { @@ -377,21 +368,13 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return prefs.getBoolean(Settings.PREF_KEY_IS_INTERNAL, false); } - public static boolean readUseOnlyPersonalizationDictionaryForDebug( - final SharedPreferences prefs) { - return prefs.getBoolean( - DebugSettings.PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG, false); - } - - public static boolean readBoostPersonalizationDictionaryForDebug( - final SharedPreferences prefs) { - return prefs.getBoolean( - DebugSettings.PREF_BOOST_PERSONALIZATION_DICTIONARY_FOR_DEBUG, false); - } - public void writeLastUsedPersonalizationToken(byte[] token) { - final String tokenStr = StringUtils.byteArrayToHexString(token); - mPrefs.edit().putString(PREF_LAST_USED_PERSONALIZATION_TOKEN, tokenStr).apply(); + if (token == null) { + mPrefs.edit().remove(PREF_LAST_USED_PERSONALIZATION_TOKEN).apply(); + } else { + final String tokenStr = StringUtils.byteArrayToHexString(token); + mPrefs.edit().putString(PREF_LAST_USED_PERSONALIZATION_TOKEN, tokenStr).apply(); + } } public byte[] readLastUsedPersonalizationToken() { @@ -399,6 +382,23 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang return StringUtils.hexStringToByteArray(tokenStr); } + public void writeLastPersonalizationDictWipedTime(final long timestamp) { + mPrefs.edit().putLong(PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME, timestamp).apply(); + } + + public long readLastPersonalizationDictGeneratedTime() { + return mPrefs.getLong(PREF_LAST_PERSONALIZATION_DICT_WIPED_TIME, 0); + } + + public void writeCorpusHandlesForPersonalization(final Set<String> corpusHandles) { + mPrefs.edit().putStringSet(PREF_CORPUS_HANDLES_FOR_PERSONALIZATION, corpusHandles).apply(); + } + + public Set<String> readCorpusHandlesForPersonalization() { + final Set<String> emptySet = Collections.emptySet(); + return mPrefs.getStringSet(PREF_CORPUS_HANDLES_FOR_PERSONALIZATION, emptySet); + } + public static void writeEmojiRecentKeys(final SharedPreferences prefs, String str) { prefs.edit().putString(PREF_EMOJI_RECENT_KEYS, str).apply(); } diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java index 5c60a7350..bb5547fc9 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java @@ -48,7 +48,6 @@ import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; import com.android.inputmethod.latin.utils.ApplicationUtils; import com.android.inputmethod.latin.utils.FeedbackUtils; import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; -import com.android.inputmethod.research.ResearchLogger; import com.android.inputmethodcommon.InputMethodSettingsFragment; import java.util.TreeSet; @@ -61,13 +60,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS || Build.VERSION.SDK_INT <= 18 /* Build.VERSION.JELLY_BEAN_MR2 */; - private CheckBoxPreference mVoiceInputKeyPreference; - private ListPreference mShowCorrectionSuggestionsPreference; - private ListPreference mAutoCorrectionThresholdPreference; - private ListPreference mKeyPreviewPopupDismissDelay; - // Use bigrams to predict the next word when there is no input for it yet - private CheckBoxPreference mBigramPrediction; - private void setPreferenceEnabled(final String preferenceKey, final boolean enabled) { final Preference preference = findPreference(preferenceKey); if (preference != null) { @@ -75,6 +67,18 @@ public final class SettingsFragment extends InputMethodSettingsFragment } } + private void updateListPreferenceSummaryToCurrentValue(final String prefKey) { + // Because the "%s" summary trick of {@link ListPreference} doesn't work properly before + // KitKat, we need to update the summary programmatically. + final ListPreference listPreference = (ListPreference)findPreference(prefKey); + if (listPreference == null) { + return; + } + final CharSequence entries[] = listPreference.getEntries(); + final int entryIndex = listPreference.findIndexOfValue(listPreference.getValue()); + listPreference.setSummary(entryIndex < 0 ? null : entries[entryIndex]); + } + private static void removePreference(final String preferenceKey, final PreferenceGroup parent) { if (parent == null) { return; @@ -94,7 +98,7 @@ public final class SettingsFragment extends InputMethodSettingsFragment final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { preferenceScreen.setTitle( - ApplicationUtils.getAcitivityTitleResId(getActivity(), SettingsActivity.class)); + ApplicationUtils.getActivityTitleResId(getActivity(), SettingsActivity.class)); } final Resources res = getResources(); @@ -107,16 +111,9 @@ public final class SettingsFragment extends InputMethodSettingsFragment SubtypeLocaleUtils.init(context); AudioAndHapticFeedbackManager.init(context); - mVoiceInputKeyPreference = - (CheckBoxPreference) findPreference(Settings.PREF_VOICE_INPUT_KEY); - mShowCorrectionSuggestionsPreference = - (ListPreference) findPreference(Settings.PREF_SHOW_SUGGESTIONS_SETTING); final SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); prefs.registerOnSharedPreferenceChangeListener(this); - mAutoCorrectionThresholdPreference = - (ListPreference) findPreference(Settings.PREF_AUTO_CORRECTION_THRESHOLD); - mBigramPrediction = (CheckBoxPreference) findPreference(Settings.PREF_BIGRAM_PREDICTIONS); ensureConsistencyOfAutoCorrectionSettings(); final PreferenceGroup generalSettings = @@ -143,12 +140,7 @@ public final class SettingsFragment extends InputMethodSettingsFragment feedbackSettings.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(final Preference pref) { - if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { - // Use development-only feedback mechanism - ResearchLogger.getInstance().presentFeedbackDialogFromSettings(); - } else { - FeedbackUtils.showFeedbackForm(getActivity()); - } + FeedbackUtils.showFeedbackForm(getActivity()); return true; } }); @@ -167,7 +159,7 @@ public final class SettingsFragment extends InputMethodSettingsFragment final boolean showVoiceKeyOption = res.getBoolean( R.bool.config_enable_show_voice_key_option); if (!showVoiceKeyOption) { - generalSettings.removePreference(mVoiceInputKeyPreference); + removePreference(Settings.PREF_VOICE_INPUT_KEY, generalSettings); } final PreferenceGroup advancedSettings = @@ -177,26 +169,28 @@ public final class SettingsFragment extends InputMethodSettingsFragment removePreference(Settings.PREF_VIBRATION_DURATION_SETTINGS, advancedSettings); } - mKeyPreviewPopupDismissDelay = - (ListPreference) findPreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); - if (!Settings.readFromBuildConfigIfToShowKeyPreviewPopupSettingsOption(res)) { + // TODO: consolidate key preview dismiss delay with the key preview animation parameters. + if (!Settings.readFromBuildConfigIfToShowKeyPreviewPopupOption(res)) { removePreference(Settings.PREF_POPUP_ON, generalSettings); removePreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY, advancedSettings); } else { + // TODO: Cleanup this setup. + final ListPreference keyPreviewPopupDismissDelay = + (ListPreference) findPreference(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger( R.integer.config_key_preview_linger_timeout)); - mKeyPreviewPopupDismissDelay.setEntries(new String[] { + keyPreviewPopupDismissDelay.setEntries(new String[] { res.getString(R.string.key_preview_popup_dismiss_no_delay), res.getString(R.string.key_preview_popup_dismiss_default_delay), }); - mKeyPreviewPopupDismissDelay.setEntryValues(new String[] { + keyPreviewPopupDismissDelay.setEntryValues(new String[] { "0", popupDismissDelayDefaultValue }); - if (null == mKeyPreviewPopupDismissDelay.getValue()) { - mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue); + if (null == keyPreviewPopupDismissDelay.getValue()) { + keyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue); } - mKeyPreviewPopupDismissDelay.setEnabled( + keyPreviewPopupDismissDelay.setEnabled( Settings.readKeyPreviewPopupEnabled(prefs, res)); } @@ -243,20 +237,25 @@ public final class SettingsFragment extends InputMethodSettingsFragment @Override public void onResume() { super.onResume(); - final boolean isShortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled(); - if (!isShortcutImeEnabled) { - getPreferenceScreen().removePreference(mVoiceInputKeyPreference); - } final SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); + final Resources res = getResources(); + final Preference voiceInputKeyOption = findPreference(Settings.PREF_VOICE_INPUT_KEY); + if (voiceInputKeyOption != null) { + final boolean isShortcutImeEnabled = SubtypeSwitcher.getInstance() + .isShortcutImeEnabled(); + voiceInputKeyOption.setEnabled(isShortcutImeEnabled); + voiceInputKeyOption.setSummary(isShortcutImeEnabled ? null + : res.getText(R.string.voice_input_disabled_summary)); + } final CheckBoxPreference showSetupWizardIcon = (CheckBoxPreference)findPreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON); if (showSetupWizardIcon != null) { showSetupWizardIcon.setChecked(Settings.readShowSetupWizardIcon(prefs, getActivity())); } - updateShowCorrectionSuggestionsSummary(); - updateKeyPreviewPopupDelaySummary(); - updateColorSchemeSummary(prefs, getResources()); - updateCustomInputStylesSummary(); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_SHOW_SUGGESTIONS_SETTING); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEYBOARD_LAYOUT); + updateCustomInputStylesSummary(prefs, res); } @Override @@ -287,50 +286,26 @@ public final class SettingsFragment extends InputMethodSettingsFragment LauncherIconVisibilityManager.updateSetupWizardIconVisibility(getActivity()); } ensureConsistencyOfAutoCorrectionSettings(); - updateShowCorrectionSuggestionsSummary(); - updateKeyPreviewPopupDelaySummary(); - updateColorSchemeSummary(prefs, res); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_SHOW_SUGGESTIONS_SETTING); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY); + updateListPreferenceSummaryToCurrentValue(Settings.PREF_KEYBOARD_LAYOUT); refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, getResources()); } private void ensureConsistencyOfAutoCorrectionSettings() { final String autoCorrectionOff = getResources().getString( R.string.auto_correction_threshold_mode_index_off); - final String currentSetting = mAutoCorrectionThresholdPreference.getValue(); - mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff)); - } - - private void updateShowCorrectionSuggestionsSummary() { - mShowCorrectionSuggestionsPreference.setSummary( - getResources().getStringArray(R.array.prefs_suggestion_visibilities) - [mShowCorrectionSuggestionsPreference.findIndexOfValue( - mShowCorrectionSuggestionsPreference.getValue())]); + final ListPreference autoCorrectionThresholdPref = (ListPreference)findPreference( + Settings.PREF_AUTO_CORRECTION_THRESHOLD); + final String currentSetting = autoCorrectionThresholdPref.getValue(); + setPreferenceEnabled( + Settings.PREF_BIGRAM_PREDICTIONS, !currentSetting.equals(autoCorrectionOff)); } - private void updateColorSchemeSummary(final SharedPreferences prefs, final Resources res) { - // Because the "%s" summary trick of {@link ListPreference} doesn't work properly before - // KitKat, we need to update the summary by code. - final Preference preference = findPreference(Settings.PREF_KEYBOARD_LAYOUT); - if (!(preference instanceof ListPreference)) { - Log.w(TAG, "Can't find Keyboard Color Scheme preference"); - return; - } - final ListPreference colorSchemePreference = (ListPreference)preference; - final int themeIndex = Settings.readKeyboardThemeIndex(prefs, res); - int entryIndex = colorSchemePreference.findIndexOfValue(Integer.toString(themeIndex)); - if (entryIndex < 0) { - final int defaultThemeIndex = Settings.resetAndGetDefaultKeyboardThemeIndex(prefs, res); - entryIndex = colorSchemePreference.findIndexOfValue( - Integer.toString(defaultThemeIndex)); - } - colorSchemePreference.setSummary(colorSchemePreference.getEntries()[entryIndex]); - } - - private void updateCustomInputStylesSummary() { + private void updateCustomInputStylesSummary(final SharedPreferences prefs, + final Resources res) { final PreferenceScreen customInputStyles = (PreferenceScreen)findPreference(Settings.PREF_CUSTOM_INPUT_STYLES); - final SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); - final Resources res = getResources(); final String prefSubtype = Settings.readPrefAdditionalSubtypes(prefs, res); final InputMethodSubtype[] subtypes = AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtype); @@ -342,13 +317,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment customInputStyles.setSummary(styles); } - private void updateKeyPreviewPopupDelaySummary() { - final ListPreference lp = mKeyPreviewPopupDismissDelay; - final CharSequence[] entries = lp.getEntries(); - if (entries == null || entries.length <= 0) return; - lp.setSummary(entries[lp.findIndexOfValue(lp.getValue())]); - } - private void refreshEnablingsOfKeypressSoundAndVibrationSettings( final SharedPreferences sp, final Resources res) { setPreferenceEnabled(Settings.PREF_VIBRATION_DURATION_SETTINGS, diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java index f331c78e5..d47a61ed1 100644 --- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java @@ -16,27 +16,22 @@ package com.android.inputmethod.latin.settings; +import android.content.Context; import android.content.SharedPreferences; +import android.content.pm.PackageInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.util.Log; import android.view.inputmethod.EditorInfo; -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.keyboard.internal.KeySpecParser; -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.compat.AppWorkaroundsUtils; import com.android.inputmethod.latin.InputAttributes; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputMethodManager; -import com.android.inputmethod.latin.SubtypeSwitcher; -import com.android.inputmethod.latin.SuggestedWords; -import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.InputTypeUtils; -import com.android.inputmethod.latin.utils.StringUtils; - -import java.util.ArrayList; +import com.android.inputmethod.latin.utils.AsyncResultHolder; +import com.android.inputmethod.latin.utils.ResourceUtils; +import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask; + import java.util.Arrays; import java.util.Locale; @@ -50,27 +45,23 @@ public final class SettingsValues { // Float.NEGATIVE_INFINITE and Float.MAX_VALUE. Currently used for auto-correction settings. private static final String FLOAT_MAX_VALUE_MARKER_STRING = "floatMaxValue"; private static final String FLOAT_NEGATIVE_INFINITY_MARKER_STRING = "floatNegativeInfinity"; + private static final int TIMEOUT_TO_GET_TARGET_PACKAGE = 5; // seconds // From resources: + public final SpacingAndPunctuations mSpacingAndPunctuations; public final int mDelayUpdateOldSuggestions; - public final int[] mSymbolsPrecededBySpace; - public final int[] mSymbolsFollowedBySpace; - public final int[] mWordConnectors; - public final SuggestedWords mSuggestPuncList; - public final String mWordSeparators; - public final int mSentenceSeparator; - public final CharSequence mHintToSaveText; - public final boolean mCurrentLanguageHasSpaces; + public final long mDoubleSpacePeriodTimeout; // From preferences, in the same order as xml/prefs.xml: public final boolean mAutoCap; public final boolean mVibrateOn; public final boolean mSoundOn; public final boolean mKeyPreviewPopupOn; - private final boolean mShowsVoiceInputKey; + public final boolean mShowsVoiceInputKey; public final boolean mIncludesOtherImesInLanguageSwitchList; public final boolean mShowsLanguageSwitchKey; public final boolean mUseContactsDict; + public final boolean mUsePersonalizedDicts; public final boolean mUseDoubleSpacePeriod; public final boolean mBlockPotentiallyOffensive; // Use bigrams to predict the next word when there is no input for it yet @@ -94,8 +85,8 @@ public final class SettingsValues { public final float mAutoCorrectionThreshold; public final boolean mCorrectionEnabled; public final int mSuggestionVisibility; - public final boolean mBoostPersonalizationDictionaryForDebug; - public final boolean mUseOnlyPersonalizationDictionaryForDebug; + public final int mDisplayOrientation; + private final AsyncResultHolder<AppWorkaroundsUtils> mAppWorkarounds; // Setting values for additional features public final int[] mAdditionalFeaturesSettingValues = @@ -103,28 +94,17 @@ public final class SettingsValues { // Debug settings public final boolean mIsInternal; + public final int mKeyPreviewShowUpDuration; + public final int mKeyPreviewDismissDuration; + public final float mKeyPreviewShowUpStartScale; + public final float mKeyPreviewDismissEndScale; - public SettingsValues(final SharedPreferences prefs, final Locale locale, final Resources res, + public SettingsValues(final Context context, final SharedPreferences prefs, final Resources res, final InputAttributes inputAttributes) { - mLocale = locale; + mLocale = res.getConfiguration().locale; // Get the resources mDelayUpdateOldSuggestions = res.getInteger(R.integer.config_delay_update_old_suggestions); - mSymbolsPrecededBySpace = - StringUtils.toCodePointArray(res.getString(R.string.symbols_preceded_by_space)); - Arrays.sort(mSymbolsPrecededBySpace); - mSymbolsFollowedBySpace = - StringUtils.toCodePointArray(res.getString(R.string.symbols_followed_by_space)); - Arrays.sort(mSymbolsFollowedBySpace); - mWordConnectors = - StringUtils.toCodePointArray(res.getString(R.string.symbols_word_connectors)); - Arrays.sort(mWordConnectors); - final String[] suggestPuncsSpec = KeySpecParser.splitKeySpecs(res.getString( - R.string.suggested_punctuations)); - mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec); - mWordSeparators = res.getString(R.string.symbols_word_separators); - mSentenceSeparator = res.getInteger(R.integer.sentence_separator); - mHintToSaveText = res.getText(R.string.hint_add_to_dictionary); - mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces); + mSpacingAndPunctuations = new SpacingAndPunctuations(res); // Store the input attributes if (null == inputAttributes) { @@ -148,10 +128,12 @@ public final class SettingsValues { Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false); mShowsLanguageSwitchKey = Settings.readShowsLanguageSwitchKey(prefs); mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true); + mUsePersonalizedDicts = prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true); mUseDoubleSpacePeriod = prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true); mBlockPotentiallyOffensive = Settings.readBlockPotentiallyOffensive(prefs, res); mAutoCorrectEnabled = Settings.readAutoCorrectEnabled(autoCorrectionThresholdRawValue, res); mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res); + mDoubleSpacePeriodTimeout = res.getInteger(R.integer.config_double_space_period_timeout); // Compute other readable settings mKeyLongpressTimeout = Settings.readKeyLongpressTimeout(prefs, res); @@ -173,86 +155,53 @@ public final class SettingsValues { AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray( prefs, mAdditionalFeaturesSettingValues); mIsInternal = Settings.isInternal(prefs); - mBoostPersonalizationDictionaryForDebug = - Settings.readBoostPersonalizationDictionaryForDebug(prefs); - mUseOnlyPersonalizationDictionaryForDebug = - Settings.readUseOnlyPersonalizationDictionaryForDebug(prefs); - } - - // Only for tests - private SettingsValues(final Locale locale) { - // TODO: locale is saved, but not used yet. May have to change this if tests require. - mLocale = locale; - mDelayUpdateOldSuggestions = 0; - mSymbolsPrecededBySpace = new int[] { '(', '[', '{', '&' }; - Arrays.sort(mSymbolsPrecededBySpace); - mSymbolsFollowedBySpace = new int[] { '.', ',', ';', ':', '!', '?', ')', ']', '}', '&' }; - Arrays.sort(mSymbolsFollowedBySpace); - mWordConnectors = new int[] { '\'', '-' }; - Arrays.sort(mWordConnectors); - mSentenceSeparator = Constants.CODE_PERIOD; - final String[] suggestPuncsSpec = new String[] { "!", "?", ",", ":", ";" }; - mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec); - mWordSeparators = "&\t \n()[]{}*&<>+=|.,;:!?/_\""; - mHintToSaveText = "Touch again to save"; - mCurrentLanguageHasSpaces = true; - mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */); - mAutoCap = true; - mVibrateOn = true; - mSoundOn = true; - mKeyPreviewPopupOn = true; - mSlidingKeyInputPreviewEnabled = true; - mShowsVoiceInputKey = true; - mIncludesOtherImesInLanguageSwitchList = false; - mShowsLanguageSwitchKey = true; - mUseContactsDict = true; - mUseDoubleSpacePeriod = true; - mBlockPotentiallyOffensive = true; - mAutoCorrectEnabled = true; - mBigramPredictionEnabled = true; - mKeyLongpressTimeout = 300; - mKeypressVibrationDuration = 5; - mKeypressSoundVolume = 1; - mKeyPreviewPopupDismissDelay = 70; - mAutoCorrectionThreshold = 1; - mGestureInputEnabled = true; - mGestureTrailEnabled = true; - mGestureFloatingPreviewTextEnabled = true; - mPhraseGestureEnabled = true; - mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect; - mSuggestionVisibility = 0; - mIsInternal = false; - mBoostPersonalizationDictionaryForDebug = false; - mUseOnlyPersonalizationDictionaryForDebug = false; - } - - @UsedForTesting - public static SettingsValues makeDummySettingsValuesForTest(final Locale locale) { - return new SettingsValues(locale); + mKeyPreviewShowUpDuration = Settings.readKeyPreviewAnimationDuration( + prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_DURATION, + res.getInteger(R.integer.config_key_preview_show_up_duration)); + mKeyPreviewDismissDuration = Settings.readKeyPreviewAnimationDuration( + prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_DURATION, + res.getInteger(R.integer.config_key_preview_dismiss_duration)); + mKeyPreviewShowUpStartScale = Settings.readKeyPreviewAnimationScale( + prefs, DebugSettings.PREF_KEY_PREVIEW_SHOW_UP_START_SCALE, + ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_show_up_start_scale)); + mKeyPreviewDismissEndScale = Settings.readKeyPreviewAnimationScale( + prefs, DebugSettings.PREF_KEY_PREVIEW_DISMISS_END_SCALE, + ResourceUtils.getFloatFromFraction( + res, R.fraction.config_key_preview_dismiss_end_scale)); + mDisplayOrientation = res.getConfiguration().orientation; + mAppWorkarounds = new AsyncResultHolder<AppWorkaroundsUtils>(); + final PackageInfo packageInfo = TargetPackageInfoGetterTask.getCachedPackageInfo( + mInputAttributes.mTargetApplicationPackageName); + if (null != packageInfo) { + mAppWorkarounds.set(new AppWorkaroundsUtils(packageInfo)); + } else { + new TargetPackageInfoGetterTask(context, mAppWorkarounds) + .execute(mInputAttributes.mTargetApplicationPackageName); + } } public boolean isApplicationSpecifiedCompletionsOn() { return mInputAttributes.mApplicationSpecifiedCompletionOn; } - public boolean isSuggestionsRequested(final int displayOrientation) { + public boolean isSuggestionsRequested() { return mInputAttributes.mIsSettingsSuggestionStripOn - && (mCorrectionEnabled - || isSuggestionStripVisibleInOrientation(displayOrientation)); + && (mCorrectionEnabled || isSuggestionStripVisible()); } - public boolean isSuggestionStripVisibleInOrientation(final int orientation) { + public boolean isSuggestionStripVisible() { return (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_VALUE) || (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE - && orientation == Configuration.ORIENTATION_PORTRAIT); + && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT); } public boolean isWordSeparator(final int code) { - return mWordSeparators.contains(String.valueOf((char)code)); + return mSpacingAndPunctuations.isWordSeparator(code); } public boolean isWordConnector(final int code) { - return Arrays.binarySearch(mWordConnectors, code) >= 0; + return mSpacingAndPunctuations.isWordConnector(code); } public boolean isWordCodePoint(final int code) { @@ -260,24 +209,17 @@ public final class SettingsValues { } public boolean isUsuallyPrecededBySpace(final int code) { - return Arrays.binarySearch(mSymbolsPrecededBySpace, code) >= 0; + return mSpacingAndPunctuations.isUsuallyPrecededBySpace(code); } public boolean isUsuallyFollowedBySpace(final int code) { - return Arrays.binarySearch(mSymbolsFollowedBySpace, code) >= 0; + return mSpacingAndPunctuations.isUsuallyFollowedBySpace(code); } public boolean shouldInsertSpacesAutomatically() { return mInputAttributes.mShouldInsertSpacesAutomatically; } - public boolean isVoiceKeyEnabled(final EditorInfo editorInfo) { - final boolean shortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled(); - final int inputType = (editorInfo != null) ? editorInfo.inputType : 0; - return shortcutImeEnabled && mShowsVoiceInputKey - && !InputTypeUtils.isPasswordInputType(inputType); - } - public boolean isLanguageSwitchKeyEnabled() { if (!mShowsLanguageSwitchKey) { return false; @@ -294,25 +236,20 @@ public final class SettingsValues { return mInputAttributes.isSameInputType(editorInfo); } - // Helper functions to create member values. - private static SuggestedWords createSuggestPuncList(final String[] puncs) { - final ArrayList<SuggestedWordInfo> puncList = CollectionUtils.newArrayList(); - if (puncs != null) { - for (final String puncSpec : puncs) { - // TODO: Stop using KeySpceParser.getLabel(). - puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec), - SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED, - Dictionary.DICTIONARY_HARDCODED, - SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, - SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); - } - } - return new SuggestedWords(puncList, - false /* typedWordValid */, - false /* hasAutoCorrectionCandidate */, - true /* isPunctuationSuggestions */, - false /* isObsoleteSuggestions */, - false /* isPrediction */); + public boolean hasSameOrientation(final Configuration configuration) { + return mDisplayOrientation == configuration.orientation; + } + + public boolean isBeforeJellyBean() { + final AppWorkaroundsUtils appWorkaroundUtils + = mAppWorkarounds.get(null, TIMEOUT_TO_GET_TARGET_PACKAGE); + return null == appWorkaroundUtils ? false : appWorkaroundUtils.isBeforeJellyBean(); + } + + public boolean isBrokenByRecorrection() { + final AppWorkaroundsUtils appWorkaroundUtils + = mAppWorkarounds.get(null, TIMEOUT_TO_GET_TARGET_PACKAGE); + return null == appWorkaroundUtils ? false : appWorkaroundUtils.isBrokenByRecorrection(); } private static final int SUGGESTION_VISIBILITY_SHOW_VALUE = @@ -350,7 +287,7 @@ public final class SettingsValues { // When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off. final float autoCorrectionThreshold; try { - final int arrayIndex = Integer.valueOf(currentAutoCorrectionSetting); + final int arrayIndex = Integer.parseInt(currentAutoCorrectionSetting); if (arrayIndex >= 0 && arrayIndex < autoCorrectionThresholdValues.length) { final String val = autoCorrectionThresholdValues[arrayIndex]; if (FLOAT_MAX_VALUE_MARKER_STRING.equals(val)) { @@ -374,17 +311,101 @@ public final class SettingsValues { return autoCorrectionThreshold; } - private static boolean needsToShowVoiceInputKey(SharedPreferences prefs, Resources res) { - final String voiceModeMain = res.getString(R.string.voice_mode_main); - final String voiceMode = prefs.getString(Settings.PREF_VOICE_MODE_OBSOLETE, voiceModeMain); - final boolean showsVoiceInputKey = voiceMode == null || voiceMode.equals(voiceModeMain); - if (!showsVoiceInputKey) { - // Migrate settings from PREF_VOICE_MODE_OBSOLETE to PREF_VOICE_INPUT_KEY - // Set voiceModeMain as a value of obsolete voice mode settings. - prefs.edit().putString(Settings.PREF_VOICE_MODE_OBSOLETE, voiceModeMain).apply(); - // Disable voice input key. - prefs.edit().putBoolean(Settings.PREF_VOICE_INPUT_KEY, false).apply(); + private static boolean needsToShowVoiceInputKey(final SharedPreferences prefs, + final Resources res) { + if (!prefs.contains(Settings.PREF_VOICE_INPUT_KEY)) { + // Migrate preference from {@link Settings#PREF_VOICE_MODE_OBSOLETE} to + // {@link Settings#PREF_VOICE_INPUT_KEY}. + final String voiceModeMain = res.getString(R.string.voice_mode_main); + final String voiceMode = prefs.getString( + Settings.PREF_VOICE_MODE_OBSOLETE, voiceModeMain); + final boolean shouldShowVoiceInputKey = voiceModeMain.equals(voiceMode); + prefs.edit().putBoolean(Settings.PREF_VOICE_INPUT_KEY, shouldShowVoiceInputKey).apply(); + } + // Remove the obsolete preference if exists. + if (prefs.contains(Settings.PREF_VOICE_MODE_OBSOLETE)) { + prefs.edit().remove(Settings.PREF_VOICE_MODE_OBSOLETE).apply(); } return prefs.getBoolean(Settings.PREF_VOICE_INPUT_KEY, true); } + + public String dump() { + final StringBuilder sb = new StringBuilder("Current settings :"); + sb.append("\n mSpacingAndPunctuations = "); + sb.append("" + mSpacingAndPunctuations.dump()); + sb.append("\n mDelayUpdateOldSuggestions = "); + sb.append("" + mDelayUpdateOldSuggestions); + sb.append("\n mAutoCap = "); + sb.append("" + mAutoCap); + sb.append("\n mVibrateOn = "); + sb.append("" + mVibrateOn); + sb.append("\n mSoundOn = "); + sb.append("" + mSoundOn); + sb.append("\n mKeyPreviewPopupOn = "); + sb.append("" + mKeyPreviewPopupOn); + sb.append("\n mShowsVoiceInputKey = "); + sb.append("" + mShowsVoiceInputKey); + sb.append("\n mIncludesOtherImesInLanguageSwitchList = "); + sb.append("" + mIncludesOtherImesInLanguageSwitchList); + sb.append("\n mShowsLanguageSwitchKey = "); + sb.append("" + mShowsLanguageSwitchKey); + sb.append("\n mUseContactsDict = "); + sb.append("" + mUseContactsDict); + sb.append("\n mUsePersonalizedDicts = "); + sb.append("" + mUsePersonalizedDicts); + sb.append("\n mUseDoubleSpacePeriod = "); + sb.append("" + mUseDoubleSpacePeriod); + sb.append("\n mBlockPotentiallyOffensive = "); + sb.append("" + mBlockPotentiallyOffensive); + sb.append("\n mBigramPredictionEnabled = "); + sb.append("" + mBigramPredictionEnabled); + sb.append("\n mGestureInputEnabled = "); + sb.append("" + mGestureInputEnabled); + sb.append("\n mGestureTrailEnabled = "); + sb.append("" + mGestureTrailEnabled); + sb.append("\n mGestureFloatingPreviewTextEnabled = "); + sb.append("" + mGestureFloatingPreviewTextEnabled); + sb.append("\n mSlidingKeyInputPreviewEnabled = "); + sb.append("" + mSlidingKeyInputPreviewEnabled); + sb.append("\n mPhraseGestureEnabled = "); + sb.append("" + mPhraseGestureEnabled); + sb.append("\n mKeyLongpressTimeout = "); + sb.append("" + mKeyLongpressTimeout); + sb.append("\n mLocale = "); + sb.append("" + mLocale); + sb.append("\n mInputAttributes = "); + sb.append("" + mInputAttributes); + sb.append("\n mKeypressVibrationDuration = "); + sb.append("" + mKeypressVibrationDuration); + sb.append("\n mKeypressSoundVolume = "); + sb.append("" + mKeypressSoundVolume); + sb.append("\n mKeyPreviewPopupDismissDelay = "); + sb.append("" + mKeyPreviewPopupDismissDelay); + sb.append("\n mAutoCorrectEnabled = "); + sb.append("" + mAutoCorrectEnabled); + sb.append("\n mAutoCorrectionThreshold = "); + sb.append("" + mAutoCorrectionThreshold); + sb.append("\n mCorrectionEnabled = "); + sb.append("" + mCorrectionEnabled); + sb.append("\n mSuggestionVisibility = "); + sb.append("" + mSuggestionVisibility); + sb.append("\n mDisplayOrientation = "); + sb.append("" + mDisplayOrientation); + sb.append("\n mAppWorkarounds = "); + final AppWorkaroundsUtils awu = mAppWorkarounds.get(null, 0); + sb.append("" + (null == awu ? "null" : awu.toString())); + sb.append("\n mAdditionalFeaturesSettingValues = "); + sb.append("" + Arrays.toString(mAdditionalFeaturesSettingValues)); + sb.append("\n mIsInternal = "); + sb.append("" + mIsInternal); + sb.append("\n mKeyPreviewShowUpDuration = "); + sb.append("" + mKeyPreviewShowUpDuration); + sb.append("\n mKeyPreviewDismissDuration = "); + sb.append("" + mKeyPreviewDismissDuration); + sb.append("\n mKeyPreviewShowUpStartScale = "); + sb.append("" + mKeyPreviewShowUpStartScale); + sb.append("\n mKeyPreviewDismissEndScale = "); + sb.append("" + mKeyPreviewDismissEndScale); + return sb.toString(); + } } diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java new file mode 100644 index 000000000..796921f71 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java @@ -0,0 +1,116 @@ +/* + * 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.content.res.Resources; + +import com.android.inputmethod.keyboard.internal.MoreKeySpec; +import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.PunctuationSuggestions; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.utils.StringUtils; + +import java.util.Arrays; +import java.util.Locale; + +public final class SpacingAndPunctuations { + private final int[] mSortedSymbolsPrecededBySpace; + private final int[] mSortedSymbolsFollowedBySpace; + private final int[] mSortedWordConnectors; + public final int[] mSortedWordSeparators; + public final PunctuationSuggestions mSuggestPuncList; + private final int mSentenceSeparator; + public final String mSentenceSeparatorAndSpace; + public final boolean mCurrentLanguageHasSpaces; + public final boolean mUsesAmericanTypography; + public final boolean mUsesGermanRules; + + public SpacingAndPunctuations(final Resources res) { + // To be able to binary search the code point. See {@link #isUsuallyPrecededBySpace(int)}. + mSortedSymbolsPrecededBySpace = StringUtils.toSortedCodePointArray( + res.getString(R.string.symbols_preceded_by_space)); + // To be able to binary search the code point. See {@link #isUsuallyFollowedBySpace(int)}. + mSortedSymbolsFollowedBySpace = StringUtils.toSortedCodePointArray( + res.getString(R.string.symbols_followed_by_space)); + // To be able to binary search the code point. See {@link #isWordConnector(int)}. + mSortedWordConnectors = StringUtils.toSortedCodePointArray( + res.getString(R.string.symbols_word_connectors)); + mSortedWordSeparators = StringUtils.toSortedCodePointArray( + res.getString(R.string.symbols_word_separators)); + mSentenceSeparator = res.getInteger(R.integer.sentence_separator); + mSentenceSeparatorAndSpace = new String(new int[] { + mSentenceSeparator, Constants.CODE_SPACE }, 0, 2); + mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces); + final Locale locale = res.getConfiguration().locale; + // Heuristic: we use American Typography rules because it's the most common rules for all + // English variants. German rules (not "German typography") also have small gotchas. + mUsesAmericanTypography = Locale.ENGLISH.getLanguage().equals(locale.getLanguage()); + mUsesGermanRules = Locale.GERMAN.getLanguage().equals(locale.getLanguage()); + final String[] suggestPuncsSpec = MoreKeySpec.splitKeySpecs( + res.getString(R.string.suggested_punctuations)); + mSuggestPuncList = PunctuationSuggestions.newPunctuationSuggestions(suggestPuncsSpec); + } + + public boolean isWordSeparator(final int code) { + return Arrays.binarySearch(mSortedWordSeparators, code) >= 0; + } + + public boolean isWordConnector(final int code) { + return Arrays.binarySearch(mSortedWordConnectors, code) >= 0; + } + + public boolean isWordCodePoint(final int code) { + return Character.isLetter(code) || isWordConnector(code); + } + + public boolean isUsuallyPrecededBySpace(final int code) { + return Arrays.binarySearch(mSortedSymbolsPrecededBySpace, code) >= 0; + } + + public boolean isUsuallyFollowedBySpace(final int code) { + return Arrays.binarySearch(mSortedSymbolsFollowedBySpace, code) >= 0; + } + + public boolean isSentenceSeparator(final int code) { + return code == mSentenceSeparator; + } + + public String dump() { + final StringBuilder sb = new StringBuilder(); + sb.append("mSortedSymbolsPrecededBySpace = "); + sb.append("" + Arrays.toString(mSortedSymbolsPrecededBySpace)); + sb.append("\n mSortedSymbolsFollowedBySpace = "); + sb.append("" + Arrays.toString(mSortedSymbolsFollowedBySpace)); + sb.append("\n mSortedWordConnectors = "); + sb.append("" + Arrays.toString(mSortedWordConnectors)); + sb.append("\n mSortedWordSeparators = "); + sb.append("" + Arrays.toString(mSortedWordSeparators)); + sb.append("\n mSuggestPuncList = "); + sb.append("" + mSuggestPuncList); + sb.append("\n mSentenceSeparator = "); + sb.append("" + mSentenceSeparator); + sb.append("\n mSentenceSeparatorAndSpace = "); + sb.append("" + mSentenceSeparatorAndSpace); + sb.append("\n mCurrentLanguageHasSpaces = "); + sb.append("" + mCurrentLanguageHasSpaces); + sb.append("\n mUsesAmericanTypography = "); + sb.append("" + mUsesAmericanTypography); + sb.append("\n mUsesGermanRules = "); + sb.append("" + mUsesGermanRules); + return sb.toString(); + } +} diff --git a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java index c4a813c24..5072fabd6 100644 --- a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java +++ b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java @@ -38,7 +38,7 @@ import com.android.inputmethod.compat.ViewCompatUtils; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.settings.SettingsActivity; import com.android.inputmethod.latin.utils.CollectionUtils; -import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper; +import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper; import java.util.ArrayList; @@ -74,21 +74,21 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL private SettingsPoolingHandler mHandler; private static final class SettingsPoolingHandler - extends StaticInnerHandlerWrapper<SetupWizardActivity> { + extends LeakGuardHandlerWrapper<SetupWizardActivity> { private static final int MSG_POLLING_IME_SETTINGS = 0; private static final long IME_SETTINGS_POLLING_INTERVAL = 200; private final InputMethodManager mImmInHandler; - public SettingsPoolingHandler(final SetupWizardActivity outerInstance, + public SettingsPoolingHandler(final SetupWizardActivity ownerInstance, final InputMethodManager imm) { - super(outerInstance); + super(ownerInstance); mImmInHandler = imm; } @Override public void handleMessage(final Message msg) { - final SetupWizardActivity setupWizardActivity = getOuterInstance(); + final SetupWizardActivity setupWizardActivity = getOwnerInstance(); if (setupWizardActivity == null) { return; } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java index 503b18b1b..6a52481b9 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -37,6 +37,7 @@ import com.android.inputmethod.latin.SynchronouslyLoadedContactsBinaryDictionary import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary; 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.StringUtils; @@ -267,6 +268,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService // if it doesn't. See documentation for binarySearch. final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1; + // Weak <- insertIndex == 0, ..., insertIndex == mLength -> Strong if (insertIndex == 0 && mLength >= mMaxLength) { // In the future, we may want to keep track of the best suggestion score even if // we are asked for 0 suggestions. In this case, we can use the following @@ -284,11 +286,6 @@ public final class AndroidSpellCheckerService extends SpellCheckerService // } return true; } - if (insertIndex >= mMaxLength) { - // We found a suggestion, but its score is too weak to be kept considering - // the suggestion limit. - return true; - } final String wordString = new String(word, wordOffset, wordLength); if (mLength < mMaxLength) { @@ -296,12 +293,13 @@ public final class AndroidSpellCheckerService extends SpellCheckerService ++mLength; System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen); mSuggestions.add(insertIndex, wordString); + mScores[insertIndex] = score; } else { - System.arraycopy(mScores, 1, mScores, 0, insertIndex); + System.arraycopy(mScores, 1, mScores, 0, insertIndex - 1); mSuggestions.add(insertIndex, wordString); mSuggestions.remove(0); + mScores[insertIndex - 1] = score; } - mScores[insertIndex] = score; return true; } @@ -320,7 +318,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService hasRecommendedSuggestions = false; } else { gatheredSuggestions = EMPTY_STRING_ARRAY; - final float normalizedScore = BinaryDictionary.calcNormalizedScore( + final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( mOriginalText, mBestSuggestion, mBestScore); hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold); } @@ -355,7 +353,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService final int bestScore = mScores[mLength - 1]; final String bestSuggestion = mSuggestions.get(0); final float normalizedScore = - BinaryDictionary.calcNormalizedScore( + BinaryDictionaryUtils.calcNormalizedScore( mOriginalText, bestSuggestion.toString(), bestScore); hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold); if (DBG) { @@ -383,6 +381,8 @@ public final class AndroidSpellCheckerService extends SpellCheckerService new Thread("spellchecker_close_dicts") { @Override public void run() { + // Contacts dictionary can be closed multiple times here. If the dictionary is + // already closed, extra closings are no-ops, so it's safe. for (DictionaryPool pool : oldPools.values()) { pool.close(); } @@ -428,7 +428,7 @@ public final class AndroidSpellCheckerService extends SpellCheckerService final String localeStr = locale.toString(); UserBinaryDictionary userDictionary = mUserDictionaries.get(localeStr); if (null == userDictionary) { - userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true); + userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, locale, true); mUserDictionaries.put(localeStr, userDictionary); } dictionaryCollection.addDictionary(userDictionary); diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java index d6e5b75ad..69d092751 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java @@ -28,11 +28,13 @@ import android.view.textservice.SuggestionsInfo; import android.view.textservice.TextInfo; import com.android.inputmethod.compat.SuggestionsInfoCompatUtils; +import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.WordComposer; import com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService.SuggestionsGatherer; +import com.android.inputmethod.latin.utils.CoordinateUtils; import com.android.inputmethod.latin.utils.LocaleUtils; import com.android.inputmethod.latin.utils.StringUtils; @@ -312,16 +314,21 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session { false /* reportAsTypo */); } final WordComposer composer = new WordComposer(); - final int length = text.length(); - for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) { - final int codePoint = text.codePointAt(i); - composer.addKeyInfo(codePoint, dictInfo.getKeyboard(codePoint)); + final int[] codePoints = StringUtils.toCodePointArray(text); + final int[] coordinates; + if (null == dictInfo.mKeyboard) { + coordinates = CoordinateUtils.newCoordinateArray(codePoints.length, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } else { + coordinates = dictInfo.mKeyboard.getCoordinates(codePoints); } + composer.setComposingWord(codePoints, coordinates, null /* previousWord */); // TODO: make a spell checker option to block offensive words or not final ArrayList<SuggestedWordInfo> suggestions = dictInfo.mDictionary.getSuggestions(composer, prevWord, dictInfo.getProximityInfo(), true /* blockOffensiveWords */, - null /* additionalFeaturesOptions */); + null /* additionalFeaturesOptions */, + null /* inOutLanguageWeight */); if (suggestions != null) { for (final SuggestedWordInfo suggestion : suggestions) { final String suggestionStr = suggestion.mWord; diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java b/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java index b77f3e2c5..1ffe50681 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java @@ -27,7 +27,7 @@ import com.android.inputmethod.keyboard.ProximityInfo; */ public final class DictAndKeyboard { public final Dictionary mDictionary; - private final Keyboard mKeyboard; + public final Keyboard mKeyboard; private final Keyboard mManualShiftedKeyboard; public DictAndKeyboard( @@ -43,13 +43,6 @@ public final class DictAndKeyboard { keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED); } - public Keyboard getKeyboard(final int codePoint) { - if (mKeyboard == null) { - return null; - } - return mKeyboard.getKey(codePoint) != null ? mKeyboard : mManualShiftedKeyboard; - } - public ProximityInfo getProximityInfo() { return mKeyboard == null ? null : mKeyboard.getProximityInfo(); } diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java index a0aed2829..c99264347 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java @@ -49,10 +49,12 @@ public final class DictionaryPool extends LinkedBlockingQueue<DictAndKeyboard> { final static ArrayList<SuggestedWordInfo> noSuggestions = CollectionUtils.newArrayList(); private final static DictAndKeyboard dummyDict = new DictAndKeyboard( new Dictionary(Dictionary.TYPE_MAIN) { + // TODO: this dummy dictionary should be a singleton in the Dictionary class. @Override public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer, final String prevWord, final ProximityInfo proximityInfo, - final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) { + final boolean blockOffensiveWords, final int[] additionalFeaturesOptions, + final float[] inOutLanguageWeight) { return noSuggestions; } @Override diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java index 999ca775b..186dafd29 100644 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java @@ -39,7 +39,7 @@ public final class SpellCheckerSettingsFragment extends PreferenceFragment { addPreferencesFromResource(R.xml.spell_checker_settings); final PreferenceScreen preferenceScreen = getPreferenceScreen(); if (preferenceScreen != null) { - preferenceScreen.setTitle(ApplicationUtils.getAcitivityTitleResId( + preferenceScreen.setTitle(ApplicationUtils.getActivityTitleResId( getActivity(), SpellCheckerSettingsActivity.class)); } } diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java index acd47450b..a104baa08 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java @@ -47,10 +47,10 @@ public final class MoreSuggestions extends Keyboard { } private static final class MoreSuggestionsParam extends KeyboardParams { - private final int[] mWidths = new int[SuggestionStripView.MAX_SUGGESTIONS]; - private final int[] mRowNumbers = new int[SuggestionStripView.MAX_SUGGESTIONS]; - private final int[] mColumnOrders = new int[SuggestionStripView.MAX_SUGGESTIONS]; - private final int[] mNumColumnsInRow = new int[SuggestionStripView.MAX_SUGGESTIONS]; + private final int[] mWidths = new int[SuggestedWords.MAX_SUGGESTIONS]; + private final int[] mRowNumbers = new int[SuggestedWords.MAX_SUGGESTIONS]; + private final int[] mColumnOrders = new int[SuggestedWords.MAX_SUGGESTIONS]; + private final int[] mNumColumnsInRow = new int[SuggestedWords.MAX_SUGGESTIONS]; private static final int MAX_COLUMNS_IN_ROW = 3; private int mNumRows; public Drawable mDivider; @@ -66,16 +66,17 @@ public final class MoreSuggestions extends Keyboard { clearKeys(); mDivider = res.getDrawable(R.drawable.more_suggestions_divider); mDividerWidth = mDivider.getIntrinsicWidth(); - final float padding = res.getDimension(R.dimen.more_suggestions_key_horizontal_padding); + final float padding = res.getDimension( + R.dimen.config_more_suggestions_key_horizontal_padding); int row = 0; int index = fromIndex; int rowStartIndex = fromIndex; - final int size = Math.min(suggestedWords.size(), SuggestionStripView.MAX_SUGGESTIONS); + final int size = Math.min(suggestedWords.size(), SuggestedWords.MAX_SUGGESTIONS); while (index < size) { - final String word = suggestedWords.getWord(index); + final String word = suggestedWords.getLabel(index); // TODO: Should take care of text x-scaling. - mWidths[index] = (int)(TypefaceUtils.getLabelWidth(word, paint) + padding); + mWidths[index] = (int)(TypefaceUtils.getStringWidth(word, paint) + padding); final int numColumn = index - rowStartIndex + 1; final int columnWidth = (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn; @@ -205,13 +206,13 @@ public final class MoreSuggestions extends Keyboard { final int x = params.getX(index); final int y = params.getY(index); final int width = params.getWidth(index); - final String word = mSuggestedWords.getWord(index); + final String word = mSuggestedWords.getLabel(index); final String info = mSuggestedWords.getDebugString(index); final int indexInMoreSuggestions = index + SUGGESTION_CODE_BASE; - final Key key = new Key( - params, word, info, KeyboardIconsSet.ICON_UNDEFINED, indexInMoreSuggestions, - null /* outputText */, x, y, width, params.mDefaultRowHeight, - 0 /* labelFlags */, Key.BACKGROUND_TYPE_NORMAL); + final Key key = new Key(word, KeyboardIconsSet.ICON_UNDEFINED, + indexInMoreSuggestions, null /* outputText */, info, 0 /* labelFlags */, + Key.BACKGROUND_TYPE_NORMAL, x, y, width, params.mDefaultRowHeight, + params.mHorizontalGap, params.mVerticalGap); params.markAsEdgeKey(key, index); params.onAddKey(key); final int columnNumber = params.getColumnNumber(index); diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java index 0ebe37782..549ff0d9d 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java @@ -54,7 +54,7 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView { public void adjustVerticalCorrectionForModalMode() { // Set vertical correction to zero (Reset more keys keyboard sliding allowance - // {@link R#dimen.more_keys_keyboard_slide_allowance}). + // {@link R#dimen.config_more_keys_keyboard_slide_allowance}). mKeyDetector.setKeyboard(getKeyboard(), -getPaddingLeft(), -getPaddingTop()); } diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java index faa5560e4..1d84bb59f 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java @@ -28,6 +28,7 @@ import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.support.v4.view.ViewCompat; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; @@ -38,18 +39,18 @@ import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import android.util.AttributeSet; import android.view.Gravity; -import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; import com.android.inputmethod.latin.LatinImeLogger; +import com.android.inputmethod.latin.PunctuationSuggestions; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.utils.AutoCorrectionUtils; import com.android.inputmethod.latin.utils.ResourceUtils; +import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; import com.android.inputmethod.latin.utils.ViewLayoutUtils; import java.util.ArrayList; @@ -64,7 +65,7 @@ final class SuggestionStripLayoutHelper { public final int mPadding; public final int mDividerWidth; public final int mSuggestionsStripHeight; - public final int mSuggestionsCountInStrip; + private final int mSuggestionsCountInStrip; public final int mMoreSuggestionsRowHeight; private int mMaxMoreSuggestionsRow; public final float mMinMoreSuggestionsWidth; @@ -89,21 +90,18 @@ final class SuggestionStripLayoutHelper { private final Drawable mMoreSuggestionsHint; private static final String MORE_SUGGESTIONS_HINT = "\u2026"; private static final String LEFTWARDS_ARROW = "\u2190"; + private static final String RIGHTWARDS_ARROW = "\u2192"; private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); - private final int mSuggestionStripOption; + private final int mSuggestionStripOptions; // These constants are the flag values of - // {@link R.styleable#SuggestionStripView_suggestionStripOption} attribute. + // {@link R.styleable#SuggestionStripView_suggestionStripOptions} attribute. private static final int AUTO_CORRECT_BOLD = 0x01; private static final int AUTO_CORRECT_UNDERLINE = 0x02; private static final int VALID_TYPED_WORD_BOLD = 0x04; - private final TextView mWordToSaveView; - private final TextView mLeftwardsArrowView; - private final TextView mHintToSaveView; - public SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs, final int defStyle, final ArrayList<TextView> wordViews, final ArrayList<View> dividerViews, final ArrayList<TextView> debugInfoViews) { @@ -119,12 +117,13 @@ final class SuggestionStripLayoutHelper { mDividerWidth = dividerView.getMeasuredWidth(); final Resources res = wordView.getResources(); - mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height); + mSuggestionsStripHeight = res.getDimensionPixelSize( + R.dimen.config_suggestions_strip_height); final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripView); - mSuggestionStripOption = a.getInt( - R.styleable.SuggestionStripView_suggestionStripOption, 0); + mSuggestionStripOptions = a.getInt( + R.styleable.SuggestionStripView_suggestionStripOptions, 0); mAlphaObsoleted = ResourceUtils.getFraction(a, R.styleable.SuggestionStripView_alphaObsoleted, 1.0f); mColorValidTypedWord = a.getColor(R.styleable.SuggestionStripView_colorValidTypedWord, 0); @@ -145,20 +144,17 @@ final class SuggestionStripLayoutHelper { a.recycle(); mMoreSuggestionsHint = getMoreSuggestionsHint(res, - res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect); + res.getDimension(R.dimen.config_more_suggestions_hint_text_size), + mColorAutoCorrect); mCenterPositionInStrip = mSuggestionsCountInStrip / 2; // Assuming there are at least three suggestions. Also, note that the suggestions are // laid out according to script direction, so this is left of the center for LTR scripts // and right of the center for RTL scripts. mTypedWordPositionWhenAutocorrect = mCenterPositionInStrip - 1; mMoreSuggestionsBottomGap = res.getDimensionPixelOffset( - R.dimen.more_suggestions_bottom_gap); - mMoreSuggestionsRowHeight = res.getDimensionPixelSize(R.dimen.more_suggestions_row_height); - - final LayoutInflater inflater = LayoutInflater.from(context); - mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null); - mLeftwardsArrowView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null); - mHintToSaveView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null); + R.dimen.config_more_suggestions_bottom_gap); + mMoreSuggestionsRowHeight = res.getDimensionPixelSize( + R.dimen.config_more_suggestions_row_height); } public int getMaxMoreSuggestionsRow() { @@ -203,23 +199,25 @@ final class SuggestionStripLayoutHelper { if (indexInSuggestedWords >= suggestedWords.size()) { return null; } - final String word = suggestedWords.getWord(indexInSuggestedWords); - final boolean isAutoCorrect = indexInSuggestedWords == 1 - && suggestedWords.willAutoCorrect(); - final boolean isTypedWordValid = indexInSuggestedWords == 0 - && suggestedWords.mTypedWordValid; - if (!isAutoCorrect && !isTypedWordValid) { + final String word = suggestedWords.getLabel(indexInSuggestedWords); + // TODO: don't use the index to decide whether this is the auto-correction/typed word, as + // this is brittle + final boolean isAutoCorrection = suggestedWords.mWillAutoCorrect + && indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION; + final boolean isTypedWordValid = suggestedWords.mTypedWordValid + && indexInSuggestedWords == SuggestedWords.INDEX_OF_TYPED_WORD; + if (!isAutoCorrection && !isTypedWordValid) { return word; } final int len = word.length(); final Spannable spannedWord = new SpannableString(word); - final int option = mSuggestionStripOption; - if ((isAutoCorrect && (option & AUTO_CORRECT_BOLD) != 0) - || (isTypedWordValid && (option & VALID_TYPED_WORD_BOLD) != 0)) { + 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); } - if (isAutoCorrect && (option & AUTO_CORRECT_UNDERLINE) != 0) { + if (isAutoCorrection && (options & AUTO_CORRECT_UNDERLINE) != 0) { spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } return spannedWord; @@ -229,7 +227,7 @@ final class SuggestionStripLayoutHelper { final SuggestedWords suggestedWords) { final int indexToDisplayMostImportantSuggestion; final int indexToDisplaySecondMostImportantSuggestion; - if (suggestedWords.willAutoCorrect()) { + if (suggestedWords.mWillAutoCorrect) { indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; } else { @@ -246,35 +244,36 @@ final class SuggestionStripLayoutHelper { return indexInSuggestedWords; } - private int getSuggestionTextColor(final int indexInSuggestedWords, - final SuggestedWords suggestedWords) { + private int getSuggestionTextColor(final SuggestedWords suggestedWords, + final int indexInSuggestedWords) { final int positionInStrip = getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords); - // TODO: Need to revisit this logic with bigram suggestions - final boolean isSuggested = (indexInSuggestedWords != SuggestedWords.INDEX_OF_TYPED_WORD); + // Use identity for strings, not #equals : it's the typed word if it's the same object + final boolean isTypedWord = + suggestedWords.getWord(indexInSuggestedWords) == suggestedWords.mTypedWord; final int color; - if (positionInStrip == mCenterPositionInStrip && suggestedWords.willAutoCorrect()) { + if (positionInStrip == mCenterPositionInStrip && suggestedWords.mWillAutoCorrect) { color = mColorAutoCorrect; - } else if (positionInStrip == mCenterPositionInStrip && suggestedWords.mTypedWordValid) { + } else if (isTypedWord && suggestedWords.mTypedWordValid) { color = mColorValidTypedWord; - } else if (isSuggested) { - color = mColorSuggested; - } else { + } else if (isTypedWord) { color = mColorTypedWord; + } else { + color = mColorSuggested; } if (LatinImeLogger.sDBG && suggestedWords.size() > 1) { // If we auto-correct, then the autocorrection is in slot 0 and the typed word // is in slot 1. if (positionInStrip == mCenterPositionInStrip && AutoCorrectionUtils.shouldBlockAutoCorrectionBySafetyNet( - suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION), - suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD))) { + suggestedWords.getLabel(SuggestedWords.INDEX_OF_AUTO_CORRECTION), + suggestedWords.getLabel(SuggestedWords.INDEX_OF_TYPED_WORD))) { return 0xFFFF0000; } } - if (suggestedWords.mIsObsoleteSuggestions && isSuggested) { + if (suggestedWords.mIsObsoleteSuggestions && !isTypedWord) { return applyAlpha(color, mAlphaObsoleted); } return color; @@ -292,54 +291,65 @@ final class SuggestionStripLayoutHelper { params.gravity = Gravity.CENTER; } - public void layout(final SuggestedWords suggestedWords, final ViewGroup stripView, - final ViewGroup placerView) { - if (suggestedWords.mIsPunctuationSuggestions) { - layoutPunctuationSuggestions(suggestedWords, stripView); - return; + /** + * Layout suggestions to the suggestions strip. And returns the number of suggestions displayed + * in the suggestions strip. + * + * @param suggestedWords suggestions to be shown in the suggestions strip. + * @param stripView the suggestions strip view. + * @param placerView the view where the debug info will be placed. + * @return the number of suggestions displayed in the suggestions strip + */ + public int layoutAndReturnSuggestionCountInStrip(final SuggestedWords suggestedWords, + final ViewGroup stripView, final ViewGroup placerView) { + if (suggestedWords.isPunctuationSuggestions()) { + return layoutPunctuationSuggestionsAndReturnSuggestionCountInStrip( + (PunctuationSuggestions)suggestedWords, stripView); } - final int countInStrip = mSuggestionsCountInStrip; - setupWordViewsTextAndColor(suggestedWords, countInStrip); + setupWordViewsTextAndColor(suggestedWords, mSuggestionsCountInStrip); final TextView centerWordView = mWordViews.get(mCenterPositionInStrip); final int availableStripWidth = placerView.getWidth() - placerView.getPaddingRight() - placerView.getPaddingLeft(); final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, availableStripWidth); - if (getTextScaleX(centerWordView.getText(), centerWidth, centerWordView.getPaint()) - < MIN_TEXT_XSCALE) { + final int countInStrip; + if (suggestedWords.size() == 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. - mMoreSuggestionsAvailable = (suggestedWords.size() > 1); + countInStrip = 1; + mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); layoutWord(mCenterPositionInStrip, availableStripWidth - mPadding); stripView.addView(centerWordView); setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT); if (SuggestionStripView.DBG) { layoutDebugInfo(mCenterPositionInStrip, placerView, availableStripWidth); } - return; - } - - mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); - int x = 0; - for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { - if (positionInStrip != 0) { - final View divider = mDividerViews.get(positionInStrip); - // Add divider if this isn't the left most suggestion in suggestions strip. - addDivider(stripView, divider); - x += divider.getMeasuredWidth(); - } - - final int width = getSuggestionWidth(positionInStrip, availableStripWidth); - final TextView wordView = layoutWord(positionInStrip, width); - stripView.addView(wordView); - setLayoutWeight(wordView, getSuggestionWeight(positionInStrip), - ViewGroup.LayoutParams.MATCH_PARENT); - x += wordView.getMeasuredWidth(); - - if (SuggestionStripView.DBG) { - layoutDebugInfo(positionInStrip, placerView, x); + } else { + countInStrip = mSuggestionsCountInStrip; + mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); + int x = 0; + for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { + if (positionInStrip != 0) { + final View divider = mDividerViews.get(positionInStrip); + // Add divider if this isn't the left most suggestion in suggestions strip. + addDivider(stripView, divider); + x += divider.getMeasuredWidth(); + } + + final int width = getSuggestionWidth(positionInStrip, availableStripWidth); + final TextView wordView = layoutWord(positionInStrip, width); + stripView.addView(wordView); + setLayoutWeight(wordView, getSuggestionWeight(positionInStrip), + ViewGroup.LayoutParams.MATCH_PARENT); + x += wordView.getMeasuredWidth(); + + if (SuggestionStripView.DBG) { + layoutDebugInfo(positionInStrip, placerView, x); + } } } + return countInStrip; } /** @@ -431,7 +441,7 @@ final class SuggestionStripLayoutHelper { // {@link SuggestionStripView#onClick(View)}. wordView.setTag(indexInSuggestedWords); wordView.setText(getStyledSuggestedWord(suggestedWords, indexInSuggestedWords)); - wordView.setTextColor(getSuggestionTextColor(positionInStrip, suggestedWords)); + wordView.setTextColor(getSuggestionTextColor(suggestedWords, indexInSuggestedWords)); if (SuggestionStripView.DBG) { mDebugInfoViews.get(positionInStrip).setText( suggestedWords.getDebugString(indexInSuggestedWords)); @@ -439,9 +449,9 @@ final class SuggestionStripLayoutHelper { } } - private void layoutPunctuationSuggestions(final SuggestedWords suggestedWords, - final ViewGroup stripView) { - final int countInStrip = Math.min(suggestedWords.size(), PUNCTUATIONS_IN_STRIP); + private int layoutPunctuationSuggestionsAndReturnSuggestionCountInStrip( + final PunctuationSuggestions punctuationSuggestions, final ViewGroup stripView) { + final int countInStrip = Math.min(punctuationSuggestions.size(), PUNCTUATIONS_IN_STRIP); for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { if (positionInStrip != 0) { // Add divider if this isn't the left most suggestion in suggestions strip. @@ -454,66 +464,63 @@ final class SuggestionStripLayoutHelper { // {@link TextView#getTag()} is used to get the index in suggestedWords at // {@link SuggestionStripView#onClick(View)}. wordView.setTag(positionInStrip); - wordView.setText(suggestedWords.getWord(positionInStrip)); + wordView.setText(punctuationSuggestions.getLabel(positionInStrip)); wordView.setTextScaleX(1.0f); wordView.setCompoundDrawables(null, null, null, null); stripView.addView(wordView); setLayoutWeight(wordView, 1.0f, mSuggestionsStripHeight); } - mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); + mMoreSuggestionsAvailable = (punctuationSuggestions.size() > countInStrip); + return countInStrip; } - public void layoutAddToDictionaryHint(final String word, final ViewGroup stripView, - final int stripWidth, final CharSequence hintText, final OnClickListener listener) { + public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip, + final int stripWidth) { final int width = stripWidth - mDividerWidth - mPadding * 2; - final TextView wordView = mWordToSaveView; + final TextView wordView = (TextView)addToDictionaryStrip.findViewById(R.id.word_to_save); wordView.setTextColor(mColorTypedWord); final int wordWidth = (int)(width * mCenterSuggestionWeight); - final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint()); + final CharSequence wordToSave = getEllipsizedText(word, wordWidth, wordView.getPaint()); final float wordScaleX = wordView.getTextScaleX(); - // {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word - // will be extracted at {@link #getAddToDictionaryWord()}. - wordView.setTag(word); - wordView.setText(text); + wordView.setText(wordToSave); wordView.setTextScaleX(wordScaleX); - stripView.addView(wordView); setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); - stripView.addView(mDividerViews.get(0)); - - final TextView leftArrowView = mLeftwardsArrowView; - leftArrowView.setTextColor(mColorAutoCorrect); - leftArrowView.setText(LEFTWARDS_ARROW); - stripView.addView(leftArrowView); - - final TextView hintView = mHintToSaveView; - hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); + final TextView hintView = (TextView)addToDictionaryStrip.findViewById( + R.id.hint_add_to_dictionary); hintView.setTextColor(mColorAutoCorrect); - final int hintWidth = width - wordWidth - leftArrowView.getWidth(); - final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint()); - hintView.setText(hintText); + final boolean isRtlLanguage = (ViewCompat.getLayoutDirection(addToDictionaryStrip) + == ViewCompat.LAYOUT_DIRECTION_RTL); + final String arrow = isRtlLanguage ? RIGHTWARDS_ARROW : LEFTWARDS_ARROW; + final Resources res = addToDictionaryStrip.getResources(); + final boolean isRtlSystem = SubtypeLocaleUtils.isRtlLanguage(res.getConfiguration().locale); + final CharSequence hintText = res.getText(R.string.hint_add_to_dictionary); + final String hintWithArrow = (isRtlLanguage == isRtlSystem) + ? (arrow + hintText) : (hintText + arrow); + final int hintWidth = width - wordWidth; + hintView.setTextScaleX(1.0f); // Reset textScaleX. + final float hintScaleX = getTextScaleX(hintWithArrow, hintWidth, hintView.getPaint()); + hintView.setText(hintWithArrow); hintView.setTextScaleX(hintScaleX); - stripView.addView(hintView); setLayoutWeight( hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); - - wordView.setOnClickListener(listener); - leftArrowView.setOnClickListener(listener); - hintView.setOnClickListener(listener); - } - - public String getAddToDictionaryWord() { - // String tag is set at - // {@link #layoutAddToDictionaryHint(String,ViewGroup,int,CharSequence,OnClickListener}. - return (String)mWordToSaveView.getTag(); } - public boolean isAddToDictionaryShowing(final View v) { - return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView; + public void layoutImportantNotice(final View importantNoticeStrip, + final String importantNoticeTitle) { + final TextView titleView = (TextView)importantNoticeStrip.findViewById( + R.id.important_notice_title); + final int width = titleView.getWidth() - titleView.getPaddingLeft() + - titleView.getPaddingRight(); + titleView.setTextColor(mColorAutoCorrect); + titleView.setText(importantNoticeTitle); + titleView.setTextScaleX(1.0f); // Reset textScaleX. + final float titleScaleX = getTextScaleX(importantNoticeTitle, width, titleView.getPaint()); + titleView.setTextScaleX(titleScaleX); } - private static void setLayoutWeight(final View v, final float weight, final int height) { + static void setLayoutWeight(final View v, final float weight, final int height) { final ViewGroup.LayoutParams lp = v.getLayoutParams(); if (lp instanceof LinearLayout.LayoutParams) { final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; @@ -527,7 +534,7 @@ final class SuggestionStripLayoutHelper { final TextPaint paint) { paint.setTextScaleX(1.0f); final int width = getTextWidth(text, paint); - if (width <= maxWidth) { + if (width <= maxWidth || maxWidth <= 0) { return 1.0f; } return maxWidth / (float)width; diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index 75f17c559..a0793b133 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -18,7 +18,11 @@ package com.android.inputmethod.latin.suggestions; import android.content.Context; import android.content.res.Resources; +import android.graphics.Color; +import android.support.v4.view.ViewCompat; +import android.text.TextUtils; import android.util.AttributeSet; +import android.util.TypedValue; import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -26,22 +30,25 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; +import android.view.ViewParent; import android.widget.RelativeLayout; import android.widget.TextView; import com.android.inputmethod.keyboard.Keyboard; -import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.keyboard.MoreKeysPanel; import com.android.inputmethod.latin.AudioAndHapticFeedbackManager; import com.android.inputmethod.latin.Constants; +import com.android.inputmethod.latin.InputAttributes; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.define.ProductionFlag; +import com.android.inputmethod.latin.settings.Settings; import com.android.inputmethod.latin.suggestions.MoreSuggestions.MoreSuggestionsListener; import com.android.inputmethod.latin.utils.CollectionUtils; +import com.android.inputmethod.latin.utils.ImportantNoticeUtils; import com.android.inputmethod.research.ResearchLogger; import java.util.ArrayList; @@ -50,15 +57,16 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick OnLongClickListener { public interface Listener { public void addWordToUserDictionary(String word); + public void showImportantNoticeContents(); public void pickSuggestionManually(int index, SuggestedWordInfo word); } - // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}. - public static final int MAX_SUGGESTIONS = 18; - static final boolean DBG = LatinImeLogger.sDBG; + private static final float DEBUG_INFO_TEXT_SIZE_IN_DIP = 6.0f; private final ViewGroup mSuggestionsStrip; + private final ViewGroup mAddToDictionaryStrip; + private final View mImportantNoticeStrip; MainKeyboardView mMainKeyboardView; private final View mMoreSuggestionsContainer; @@ -71,8 +79,54 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick Listener mListener; private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; + private int mSuggestionsCountInStrip; private final SuggestionStripLayoutHelper mLayoutHelper; + private final StripVisibilityGroup mStripVisibilityGroup; + + private static class StripVisibilityGroup { + private final View mSuggestionsStrip; + private final View mAddToDictionaryStrip; + private final View mImportantNoticeStrip; + + public StripVisibilityGroup(final View suggestionsStrip, final View addToDictionaryStrip, + final View importantNoticeStrip) { + mSuggestionsStrip = suggestionsStrip; + mAddToDictionaryStrip = addToDictionaryStrip; + mImportantNoticeStrip = importantNoticeStrip; + showSuggestionsStrip(); + } + + public void setLayoutDirection(final boolean isRtlLanguage) { + final int layoutDirection = isRtlLanguage ? ViewCompat.LAYOUT_DIRECTION_RTL + : ViewCompat.LAYOUT_DIRECTION_LTR; + ViewCompat.setLayoutDirection(mSuggestionsStrip, layoutDirection); + ViewCompat.setLayoutDirection(mAddToDictionaryStrip, layoutDirection); + ViewCompat.setLayoutDirection(mImportantNoticeStrip, layoutDirection); + } + + public void showSuggestionsStrip() { + mSuggestionsStrip.setVisibility(VISIBLE); + mAddToDictionaryStrip.setVisibility(INVISIBLE); + mImportantNoticeStrip.setVisibility(INVISIBLE); + } + + public void showAddToDictionaryStrip() { + mSuggestionsStrip.setVisibility(INVISIBLE); + mAddToDictionaryStrip.setVisibility(VISIBLE); + mImportantNoticeStrip.setVisibility(INVISIBLE); + } + + public void showImportantNoticeStrip() { + mSuggestionsStrip.setVisibility(INVISIBLE); + mAddToDictionaryStrip.setVisibility(INVISIBLE); + mImportantNoticeStrip.setVisibility(VISIBLE); + } + + public boolean isShowingAddToDictionaryStrip() { + return mAddToDictionaryStrip.getVisibility() == VISIBLE; + } + } /** * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user. @@ -91,15 +145,23 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick inflater.inflate(R.layout.suggestions_strip, this); mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip); - for (int pos = 0; pos < MAX_SUGGESTIONS; pos++) { - final TextView word = (TextView)inflater.inflate(R.layout.suggestion_word, null); + mAddToDictionaryStrip = (ViewGroup)findViewById(R.id.add_to_dictionary_strip); + mImportantNoticeStrip = findViewById(R.id.important_notice_strip); + mStripVisibilityGroup = new StripVisibilityGroup(mSuggestionsStrip, mAddToDictionaryStrip, + mImportantNoticeStrip); + + for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) { + final TextView word = new TextView(context, null, R.attr.suggestionWordStyle); word.setOnClickListener(this); word.setOnLongClickListener(this); mWordViews.add(word); final View divider = inflater.inflate(R.layout.suggestion_divider, null); divider.setOnClickListener(this); mDividerViews.add(divider); - mDebugInfoViews.add((TextView)inflater.inflate(R.layout.suggestion_info, null)); + final TextView info = new TextView(context, null, R.attr.suggestionWordStyle); + info.setTextColor(Color.WHITE); + info.setTextSize(TypedValue.COMPLEX_UNIT_DIP, DEBUG_INFO_TEXT_SIZE_IN_DIP); + mDebugInfoViews.add(info); } mLayoutHelper = new SuggestionStripLayoutHelper( @@ -112,7 +174,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick final Resources res = context.getResources(); mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset( - R.dimen.more_suggestions_modal_tolerance); + R.dimen.config_more_suggestions_modal_tolerance); mMoreSuggestionsSlidingDetector = new GestureDetector( context, mMoreSuggestionsSlidingListener); } @@ -126,13 +188,16 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick mMainKeyboardView = (MainKeyboardView)inputView.findViewById(R.id.keyboard_view); } - public void setSuggestions(final SuggestedWords suggestedWords) { + public void setSuggestions(final SuggestedWords suggestedWords, final boolean isRtlLanguage) { clear(); + mStripVisibilityGroup.setLayoutDirection(isRtlLanguage); mSuggestedWords = suggestedWords; - mLayoutHelper.layout(mSuggestedWords, mSuggestionsStrip, this); + mSuggestionsCountInStrip = mLayoutHelper.layoutAndReturnSuggestionCountInStrip( + mSuggestedWords, mSuggestionsStrip, this); if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords); } + mStripVisibilityGroup.showSuggestionsStrip(); } public int setMoreSuggestionsHeight(final int remainingHeight) { @@ -140,14 +205,16 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick } public boolean isShowingAddToDictionaryHint() { - return mSuggestionsStrip.getChildCount() > 0 - && mLayoutHelper.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0)); + return mStripVisibilityGroup.isShowingAddToDictionaryStrip(); } - public void showAddToDictionaryHint(final String word, final CharSequence hintText) { - clear(); - mLayoutHelper.layoutAddToDictionaryHint( - word, mSuggestionsStrip, getWidth(), hintText, this); + public void showAddToDictionaryHint(final String word) { + mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip, getWidth()); + // {@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); + mAddToDictionaryStrip.setOnClickListener(this); + mStripVisibilityGroup.showAddToDictionaryStrip(); } public boolean dismissAddToDictionaryHint() { @@ -158,31 +225,65 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick return false; } + // This method checks if we should show the important notice (checks on permanent storage if + // it has been shown once already or not, and if in the setup wizard). If applicable, it shows + // the notice. In all cases, it returns true if it was shown, false otherwise. + public boolean maybeShowImportantNoticeTitle(final InputAttributes inputAttributes) { + if (!ImportantNoticeUtils.shouldShowImportantNotice(getContext(), inputAttributes)) { + return false; + } + if (getWidth() <= 0) { + return false; + } + final String importantNoticeTitle = ImportantNoticeUtils.getNextImportantNoticeTitle( + getContext()); + if (TextUtils.isEmpty(importantNoticeTitle)) { + return false; + } + if (isShowingMoreSuggestionPanel()) { + dismissMoreSuggestionsPanel(); + } + mLayoutHelper.layoutImportantNotice(mImportantNoticeStrip, importantNoticeTitle); + mStripVisibilityGroup.showImportantNoticeStrip(); + mImportantNoticeStrip.setOnClickListener(this); + return true; + } + public void clear() { mSuggestionsStrip.removeAllViews(); - removeAllViews(); - addView(mSuggestionsStrip); - mMoreSuggestionsView.dismissMoreKeysPanel(); + removeAllDebugInfoViews(); + mStripVisibilityGroup.showSuggestionsStrip(); + dismissMoreSuggestionsPanel(); + } + + private void removeAllDebugInfoViews() { + // The debug info views may be placed as children views of this {@link SuggestionStripView}. + for (final View debugInfoView : mDebugInfoViews) { + final ViewParent parent = debugInfoView.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup)parent).removeView(debugInfoView); + } + } } private final MoreSuggestionsListener mMoreSuggestionsListener = new MoreSuggestionsListener() { @Override public void onSuggestionSelected(final int index, final SuggestedWordInfo wordInfo) { mListener.pickSuggestionManually(index, wordInfo); - mMoreSuggestionsView.dismissMoreKeysPanel(); + dismissMoreSuggestionsPanel(); } @Override public void onCancelInput() { - mMoreSuggestionsView.dismissMoreKeysPanel(); + dismissMoreSuggestionsPanel(); } }; private final MoreKeysPanel.Controller mMoreSuggestionsController = new MoreKeysPanel.Controller() { @Override - public void onDismissMoreKeysPanel(final MoreKeysPanel panel) { - mMainKeyboardView.onDismissMoreKeysPanel(panel); + public void onDismissMoreKeysPanel() { + mMainKeyboardView.onDismissMoreKeysPanel(); } @Override @@ -191,11 +292,19 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick } @Override - public void onCancelMoreKeysPanel(final MoreKeysPanel panel) { - mMoreSuggestionsView.dismissMoreKeysPanel(); + public void onCancelMoreKeysPanel() { + dismissMoreSuggestionsPanel(); } }; + public boolean isShowingMoreSuggestionPanel() { + return mMoreSuggestionsView.isShowingInParent(); + } + + public void dismissMoreSuggestionsPanel() { + mMoreSuggestionsView.dismissMoreKeysPanel(); + } + @Override public boolean onLongClick(final View view) { AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( @@ -204,7 +313,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick } boolean showMoreSuggestions() { - final Keyboard parentKeyboard = KeyboardSwitcher.getInstance().getKeyboard(); + final Keyboard parentKeyboard = mMainKeyboardView.getKeyboard(); if (parentKeyboard == null) { return false; } @@ -212,11 +321,17 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick if (!layoutHelper.mMoreSuggestionsAvailable) { return false; } + // Dismiss another {@link MoreKeysPanel} that may be being showed, for example + // {@link MoreKeysKeyboardView}. + mMainKeyboardView.onDismissMoreKeysPanel(); + // Dismiss all key previews and sliding key input preview that may be being showed. + mMainKeyboardView.dismissAllKeyPreviews(); + mMainKeyboardView.dismissSlidingKeyInputPreview(); final int stripWidth = getWidth(); final View container = mMoreSuggestionsContainer; final int maxWidth = stripWidth - container.getPaddingLeft() - container.getPaddingRight(); final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder; - builder.layout(mSuggestedWords, layoutHelper.mSuggestionsCountInStrip, maxWidth, + builder.layout(mSuggestedWords, mSuggestionsCountInStrip, maxWidth, (int)(maxWidth * layoutHelper.mMinMoreSuggestionsWidth), layoutHelper.getMaxMoreSuggestionsRow(), parentKeyboard); mMoreSuggestionsView.setKeyboard(builder.build()); @@ -227,20 +342,16 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick final int pointY = -layoutHelper.mMoreSuggestionsBottomGap; moreKeysPanel.showMoreKeysPanel(this, mMoreSuggestionsController, pointX, pointY, mMoreSuggestionsListener); - mMoreSuggestionsMode = MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING; mOriginX = mLastX; mOriginY = mLastY; - for (int i = 0; i < layoutHelper.mSuggestionsCountInStrip; i++) { + for (int i = 0; i < mSuggestionsCountInStrip; i++) { mWordViews.get(i).setPressed(false); } return true; } - // Working variables for onLongClick and dispatchTouchEvent. - private int mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE; - private static final int MORE_SUGGESTIONS_IN_MODAL_MODE = 0; - private static final int MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING = 1; - private static final int MORE_SUGGESTIONS_IN_SLIDING_MODE = 2; + // Working variables for {@link #onLongClick(View)} and + // {@link onInterceptTouchEvent(MotionEvent)}. private int mLastX; private int mLastY; private int mOriginX; @@ -260,36 +371,39 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick }; @Override - public boolean dispatchTouchEvent(final MotionEvent me) { + public boolean onInterceptTouchEvent(final MotionEvent me) { if (!mMoreSuggestionsView.isShowingInParent()) { mLastX = (int)me.getX(); mLastY = (int)me.getY(); - if (mMoreSuggestionsSlidingDetector.onTouchEvent(me)) { - return true; - } - return super.dispatchTouchEvent(me); + return mMoreSuggestionsSlidingDetector.onTouchEvent(me); } final int action = me.getAction(); final int index = me.getActionIndex(); final int x = (int)me.getX(index); final int y = (int)me.getY(index); - - if (mMoreSuggestionsMode == MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING) { - if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance - || mOriginY - y >= mMoreSuggestionsModalTolerance) { - // Decided to be in the sliding input mode only when the touch point has been moved - // upward. - mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_SLIDING_MODE; - } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { - // Decided to be in the modal input mode - mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE; - mMoreSuggestionsView.adjustVerticalCorrectionForModalMode(); - } + if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance + || mOriginY - y >= mMoreSuggestionsModalTolerance) { + // Decided to be in the sliding input mode only when the touch point has been moved + // upward. Further {@link MotionEvent}s will be delivered to + // {@link #onTouchEvent(MotionEvent)}. return true; } - // MORE_SUGGESTIONS_IN_SLIDING_MODE + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { + // Decided to be in the modal input mode. + mMoreSuggestionsView.adjustVerticalCorrectionForModalMode(); + } + return false; + } + + @Override + public boolean onTouchEvent(final MotionEvent me) { + // In the sliding input mode. {@link MotionEvent} should be forwarded to + // {@link MoreSuggestionsView}. + final int index = me.getActionIndex(); + final int x = (int)me.getX(index); + final int y = (int)me.getY(index); me.setLocation(mMoreSuggestionsView.translateX(x), mMoreSuggestionsView.translateY(y)); mMoreSuggestionsView.onTouchEvent(me); return true; @@ -297,31 +411,44 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick @Override public void onClick(final View view) { - if (mLayoutHelper.isAddToDictionaryShowing(view)) { - mListener.addWordToUserDictionary(mLayoutHelper.getAddToDictionaryWord()); + if (view == mImportantNoticeStrip) { + mListener.showImportantNoticeContents(); + return; + } + final Object tag = view.getTag(); + // {@link String} tag is set at {@link #showAddToDictionaryHint(String,CharSequence)}. + if (tag instanceof String) { + final String wordToSave = (String)tag; + mListener.addWordToUserDictionary(wordToSave); clear(); return; } - final Object tag = view.getTag(); - // Integer tag is set at + // {@link Integer} tag is set at // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup} - if (!(tag instanceof Integer)) { - return; - } - final int index = (Integer) tag; - if (index >= mSuggestedWords.size()) { - return; + if (tag instanceof Integer) { + final int index = (Integer) tag; + if (index >= mSuggestedWords.size()) { + return; + } + final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index); + mListener.pickSuggestionManually(index, wordInfo); } - - final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index); - mListener.pickSuggestionManually(index, wordInfo); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - mMoreSuggestionsView.dismissMoreKeysPanel(); + dismissMoreSuggestionsPanel(); + } + + @Override + protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { + // Called by the framework when the size is known. Show the important notice if applicable. + // This may be overriden by showing suggestions later, if applicable. + if (oldw <= 0 && w > 0) { + maybeShowImportantNoticeTitle(Settings.getInstance().getCurrent().mInputAttributes); + } } } diff --git a/java/src/com/android/inputmethod/event/EventDecoderSpec.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java index 303b4b4c9..52708455e 100644 --- a/java/src/com/android/inputmethod/event/EventDecoderSpec.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripViewAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 The Android Open Source Project + * 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. @@ -14,13 +14,17 @@ * limitations under the License. */ -package com.android.inputmethod.event; +package com.android.inputmethod.latin.suggestions; + +import com.android.inputmethod.latin.SuggestedWords; /** - * Class describing a decoder chain. This will depend on the language and the input medium (soft - * or hard keyboard for example). + * An object that gives basic control of a suggestion strip and some info on it. */ -public class EventDecoderSpec { - public EventDecoderSpec() { - } +public interface SuggestionStripViewAccessor { + public void showAddToDictionaryHint(final String word); + public boolean isShowingAddToDictionaryHint(); + public void dismissAddToDictionaryHint(); + public void setNeutralSuggestionStrip(); + public void showSuggestionStrip(final SuggestedWords suggestedWords); } diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java index 32c4950da..97a924d7b 100644 --- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java +++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java @@ -53,20 +53,24 @@ public class UserDictionaryList extends PreferenceFragment { } public static TreeSet<String> getUserDictionaryLocalesSet(Activity activity) { - @SuppressWarnings("deprecation") - final Cursor cursor = activity.managedQuery(UserDictionary.Words.CONTENT_URI, + final Cursor cursor = activity.getContentResolver().query(UserDictionary.Words.CONTENT_URI, new String[] { UserDictionary.Words.LOCALE }, null, null, null); final TreeSet<String> localeSet = new TreeSet<String>(); if (null == cursor) { // The user dictionary service is not present or disabled. Return null. return null; - } else if (cursor.moveToFirst()) { - final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE); - do { - final String locale = cursor.getString(columnIndex); - localeSet.add(null != locale ? locale : ""); - } while (cursor.moveToNext()); + } + try { + if (cursor.moveToFirst()) { + final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE); + do { + final String locale = cursor.getString(columnIndex); + localeSet.add(null != locale ? locale : ""); + } while (cursor.moveToNext()); + } + } finally { + cursor.close(); } if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) { // For ICS, we need to show "For all languages" in case that the keyboard locale diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java index 7571e87c5..cf2014a1a 100644 --- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java +++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java @@ -140,6 +140,11 @@ public class UserDictionarySettings extends ListFragment { } mLocale = locale; + // WARNING: The following cursor is never closed! TODO: don't put that in a member, and + // make sure all cursors are correctly closed. Also, this comes from a call to + // Activity#managedQuery, which has been deprecated for a long time (and which FORBIDS + // closing the cursor, so take care when resolving this TODO). We should either use a + // regular query and close the cursor, or switch to a LoaderManager and a CursorLoader. mCursor = createCursor(locale); TextView emptyView = (TextView) getView().findViewById(android.R.id.empty); emptyView.setText(R.string.user_dict_settings_empty_text); diff --git a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java index d87f6f3c4..2bb30a2ba 100644 --- a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java @@ -17,32 +17,43 @@ package com.android.inputmethod.latin.utils; import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE; +import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.EMOJI_CAPABLE; import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE; import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET; import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME; import android.os.Build; import android.text.TextUtils; +import android.util.Log; import android.view.inputmethod.InputMethodSubtype; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; -import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import java.util.ArrayList; +import java.util.Arrays; public final class AdditionalSubtypeUtils { + private static final String TAG = AdditionalSubtypeUtils.class.getSimpleName(); + private static final InputMethodSubtype[] EMPTY_SUBTYPE_ARRAY = new InputMethodSubtype[0]; private AdditionalSubtypeUtils() { // This utility class is not publicly instantiable. } + @UsedForTesting public static boolean isAdditionalSubtype(final InputMethodSubtype subtype) { return subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE); } private static final String LOCALE_AND_LAYOUT_SEPARATOR = ":"; + private static final int INDEX_OF_LOCALE = 0; + private static final int INDEX_OF_KEYBOARD_LAYOUT = 1; + private static final int INDEX_OF_EXTRA_VALUE = 2; + private static final int LENGTH_WITHOUT_EXTRA_VALUE = (INDEX_OF_KEYBOARD_LAYOUT + 1); + private static final int LENGTH_WITH_EXTRA_VALUE = (INDEX_OF_EXTRA_VALUE + 1); private static final String PREF_SUBTYPE_SEPARATOR = ";"; public static InputMethodSubtype createAdditionalSubtype(final String localeString, @@ -79,17 +90,6 @@ public final class AdditionalSubtypeUtils { : basePrefSubtype + LOCALE_AND_LAYOUT_SEPARATOR + extraValue; } - public static InputMethodSubtype createAdditionalSubtype(final String prefSubtype) { - final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR); - if (elems.length < 2 || elems.length > 3) { - throw new RuntimeException("Unknown additional subtype specified: " + prefSubtype); - } - final String localeString = elems[0]; - final String keyboardLayoutSetName = elems[1]; - final String extraValue = (elems.length == 3) ? elems[2] : null; - return createAdditionalSubtype(localeString, keyboardLayoutSetName, extraValue); - } - public static InputMethodSubtype[] createAdditionalSubtypesArray(final String prefSubtypes) { if (TextUtils.isEmpty(prefSubtypes)) { return EMPTY_SUBTYPE_ARRAY; @@ -98,7 +98,19 @@ public final class AdditionalSubtypeUtils { final ArrayList<InputMethodSubtype> subtypesList = CollectionUtils.newArrayList(prefSubtypeArray.length); for (final String prefSubtype : prefSubtypeArray) { - final InputMethodSubtype subtype = createAdditionalSubtype(prefSubtype); + final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR); + if (elems.length != LENGTH_WITHOUT_EXTRA_VALUE + && elems.length != LENGTH_WITH_EXTRA_VALUE) { + Log.w(TAG, "Unknown additional subtype specified: " + prefSubtype + " in " + + prefSubtypes); + continue; + } + final String localeString = elems[INDEX_OF_LOCALE]; + final String keyboardLayoutSetName = elems[INDEX_OF_KEYBOARD_LAYOUT]; + final String extraValue = (elems.length == LENGTH_WITH_EXTRA_VALUE) + ? elems[INDEX_OF_EXTRA_VALUE] : null; + final InputMethodSubtype subtype = createAdditionalSubtype( + localeString, keyboardLayoutSetName, extraValue); if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) { // Skip unknown keyboard layout subtype. This may happen when predefined keyboard // layout has been removed. @@ -137,31 +149,36 @@ public final class AdditionalSubtypeUtils { return sb.toString(); } - private static InputMethodSubtype buildInputMethodSubtype(int nameId, String localeString, - String layoutExtraValue, String additionalSubtypeExtraValue) { - // CAVEAT! If you want to change subtypeId after changing the extra values, - // you must change "getInputMethodSubtypeId". But it will remove the additional keyboard - // from the current users. So, you should be really careful to change it. - final int subtypeId = getInputMethodSubtypeId(nameId, localeString, layoutExtraValue, - additionalSubtypeExtraValue); + private static InputMethodSubtype buildInputMethodSubtype(final int nameId, + final String localeString, final String layoutExtraValue, + final String additionalSubtypeExtraValue) { + // To preserve additional subtype settings and user's selection across OS updates, subtype + // id shouldn't be changed. New attributes, such as emojiCapable, are carefully excluded + // from the calculation of subtype id. + final String compatibleExtraValue = StringUtils.joinCommaSplittableText( + layoutExtraValue, additionalSubtypeExtraValue); + final int compatibleSubtypeId = getInputMethodSubtypeId(localeString, compatibleExtraValue); final String extraValue; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - extraValue = layoutExtraValue + "," + additionalSubtypeExtraValue - + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE - + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE; + // Color Emoji is supported from KitKat. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + extraValue = StringUtils.appendToCommaSplittableTextIfNotExists( + EMOJI_CAPABLE, compatibleExtraValue); } else { - extraValue = layoutExtraValue + "," + additionalSubtypeExtraValue; + extraValue = compatibleExtraValue; } return InputMethodSubtypeCompatUtils.newInputMethodSubtype(nameId, R.drawable.ic_ime_switcher_dark, localeString, KEYBOARD_MODE, extraValue, - false, false, subtypeId); + false, false, compatibleSubtypeId); } - private static int getInputMethodSubtypeId(int nameId, String localeString, - String layoutExtraValue, String additionalSubtypeExtraValue) { - // TODO: Use InputMethodSubtypeBuilder once we use SDK version 19. - return (new InputMethodSubtype(nameId, R.drawable.ic_ime_switcher_dark, - localeString, KEYBOARD_MODE, layoutExtraValue + "," + additionalSubtypeExtraValue, - false, false)).hashCode(); + private static int getInputMethodSubtypeId(final String localeString, final String extraValue) { + // From the compatibility point of view, the calculation of subtype id has been copied from + // {@link InputMethodSubtype} of JellyBean MR2. + return Arrays.hashCode(new Object[] { + localeString, + KEYBOARD_MODE, + extraValue, + false /* isAuxiliary */, + false /* overrideImplicitlyEnabledSubtype */ }); } } diff --git a/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java index 08a2a8c5a..7a4150def 100644 --- a/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java @@ -31,7 +31,7 @@ public final class ApplicationUtils { // This utility class is not publicly instantiable. } - public static int getAcitivityTitleResId(final Context context, + public static int getActivityTitleResId(final Context context, final Class<? extends Activity> cls) { final ComponentName cn = new ComponentName(context, cls); try { @@ -62,4 +62,22 @@ public final class ApplicationUtils { } return ""; } + + /** + * A utility method to get the application's PackageInfo.versionCode + * @return the application's PackageInfo.versionCode + */ + public static int getVersionCode(final Context context) { + try { + if (context == null) { + return 0; + } + final String packageName = context.getPackageName(); + final PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); + return info.versionCode; + } catch (final NameNotFoundException e) { + Log.e(TAG, "Could not find version info.", e); + } + return 0; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java index c2e97a36f..d12aad639 100644 --- a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java +++ b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java @@ -20,7 +20,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** - * This class is a holder of a result of asynchronous computation. + * This class is a holder of the result of an asynchronous computation. * * @param <E> the type of the result. */ @@ -36,9 +36,9 @@ public class AsyncResultHolder<E> { } /** - * Sets the result value to this holder. + * Sets the result value of this holder. * - * @param result the value which is set. + * @param result the value to set. */ public void set(final E result) { synchronized(mLock) { @@ -54,12 +54,12 @@ public class AsyncResultHolder<E> { * Causes the current thread to wait unless the value is set or the specified time is elapsed. * * @param defaultValue the default value. - * @param timeOut the time to wait. - * @return if the result is set until the time limit then the result, otherwise defaultValue. + * @param timeOut the maximum time to wait. + * @return if the result is set before the time limit then the result, otherwise defaultValue. */ public E get(final E defaultValue, final long timeOut) { try { - if(mLatch.await(timeOut, TimeUnit.MILLISECONDS)) { + if (mLatch.await(timeOut, TimeUnit.MILLISECONDS)) { return mResult; } else { return defaultValue; diff --git a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java index 066c5fd32..22b9b77d2 100644 --- a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java @@ -17,16 +17,11 @@ package com.android.inputmethod.latin.utils; import com.android.inputmethod.latin.BinaryDictionary; -import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.LatinImeLogger; -import com.android.inputmethod.latin.Suggest; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; -import android.text.TextUtils; import android.util.Log; -import java.util.concurrent.ConcurrentHashMap; - public final class AutoCorrectionUtils { private static final boolean DBG = LatinImeLogger.sDBG; private static final String TAG = AutoCorrectionUtils.class.getSimpleName(); @@ -36,48 +31,6 @@ public final class AutoCorrectionUtils { // Purely static class: can't instantiate. } - public static boolean isValidWord(final Suggest suggest, final String word, - final boolean ignoreCase) { - if (TextUtils.isEmpty(word)) { - return false; - } - final ConcurrentHashMap<String, Dictionary> dictionaries = suggest.getUnigramDictionaries(); - final String lowerCasedWord = word.toLowerCase(suggest.mLocale); - for (final String key : dictionaries.keySet()) { - final Dictionary dictionary = dictionaries.get(key); - // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow - // managing to get null in here. Presumably the language is changing to a language with - // no main dictionary and the monkey manages to type a whole word before the thread - // that reads the dictionary is started or something? - // 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; - } - - public static int getMaxFrequency(final ConcurrentHashMap<String, Dictionary> dictionaries, - final String word) { - if (TextUtils.isEmpty(word)) { - return Dictionary.NOT_A_PROBABILITY; - } - int maxFreq = -1; - for (final String key : dictionaries.keySet()) { - final Dictionary dictionary = dictionaries.get(key); - if (null == dictionary) continue; - final int tempFreq = dictionary.getFrequency(word); - if (tempFreq >= maxFreq) { - maxFreq = tempFreq; - } - } - return maxFreq; - } - public static boolean suggestionExceedsAutoCorrectionThreshold( final SuggestedWordInfo suggestion, final String consideredWord, final float autoCorrectionThreshold) { @@ -87,7 +40,7 @@ public final class AutoCorrectionUtils { final int autoCorrectionSuggestionScore = suggestion.mScore; // TODO: when the normalized score of the first suggestion is nearly equals to // the normalized score of the second suggestion, behave less aggressive. - final float normalizedScore = BinaryDictionary.calcNormalizedScore( + final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( consideredWord, suggestion.mWord, autoCorrectionSuggestionScore); if (DBG) { Log.d(TAG, "Normalized " + consideredWord + "," + suggestion + "," @@ -118,9 +71,8 @@ public final class AutoCorrectionUtils { if (typedWordLength < MINIMUM_SAFETY_NET_CHAR_LENGTH) { return false; } - final int maxEditDistanceOfNativeDictionary = - (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1; - final int distance = BinaryDictionary.editDistance(typedWord, suggestion); + final int maxEditDistanceOfNativeDictionary = (typedWordLength / 2) + 1; + final int distance = BinaryDictionaryUtils.editDistance(typedWord, suggestion); if (DBG) { Log.d(TAG, "Autocorrected edit distance = " + distance + ", " + maxEditDistanceOfNativeDictionary); diff --git a/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java new file mode 100644 index 000000000..b4658b531 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java @@ -0,0 +1,137 @@ +/* + * 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 com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.BinaryDictionary; +import com.android.inputmethod.latin.makedict.DictionaryHeader; +import com.android.inputmethod.latin.makedict.UnsupportedFormatException; +import com.android.inputmethod.latin.personalization.PersonalizationHelper; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class BinaryDictionaryUtils { + private static final String TAG = BinaryDictionaryUtils.class.getSimpleName(); + + private BinaryDictionaryUtils() { + // This utility class is not publicly instantiable. + } + + static { + JniUtils.loadNativeLibrary(); + } + + 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) + throws IOException, UnsupportedFormatException { + return getHeaderWithOffsetAndLength(dictFile, 0 /* offset */, dictFile.length()); + } + + public static DictionaryHeader getHeaderWithOffsetAndLength(final File dictFile, + final long offset, final long length) throws IOException, UnsupportedFormatException { + // dictType is never used for reading the header. Passing an empty string. + final BinaryDictionary binaryDictionary = new BinaryDictionary( + dictFile.getAbsolutePath(), offset, length, + true /* useFullEditDistance */, null /* locale */, "" /* dictType */, + false /* isUpdatable */); + final DictionaryHeader header = binaryDictionary.getHeader(); + binaryDictionary.close(); + if (header == null) { + throw new IOException(); + } + return header; + } + + public static boolean renameDict(final File dictFile, final File newDictFile) { + if (dictFile.isFile()) { + return dictFile.renameTo(newDictFile); + } else if (dictFile.isDirectory()) { + final String dictName = dictFile.getName(); + final String newDictName = newDictFile.getName(); + if (newDictFile.exists()) { + return false; + } + for (final File file : dictFile.listFiles()) { + if (!file.isFile()) { + continue; + } + final String fileName = file.getName(); + final String newFileName = fileName.replaceFirst( + Pattern.quote(dictName), Matcher.quoteReplacement(newDictName)); + if (!file.renameTo(new File(dictFile, newFileName))) { + return false; + } + } + return dictFile.renameTo(newDictFile); + } + return false; + } + + public static boolean createEmptyDictFile(final String filePath, final long dictVersion, + final Locale locale, final Map<String, String> attributeMap) { + final String[] keyArray = new String[attributeMap.size()]; + final String[] valueArray = new String[attributeMap.size()]; + int index = 0; + for (final String key : attributeMap.keySet()) { + keyArray[index] = key; + valueArray[index] = attributeMap.get(key); + index++; + } + return createEmptyDictFileNative(filePath, dictVersion, locale.toString(), keyArray, + valueArray); + } + + public static float calcNormalizedScore(final String before, final String after, + final int score) { + return calcNormalizedScoreNative(StringUtils.toCodePointArray(before), + 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. + * In test mode, set timestamp is used as the current time in the native code. + * If currentTime < 0, quit the test mode and returns to using time() to get the current time. + * + * @param currentTime seconds since the unix epoch + * @return current time got in the native code. + */ + @UsedForTesting + public static int setCurrentTimeForTest(final int currentTime) { + final int currentNativeTimestamp = setCurrentTimeForTestNative(currentTime); + PersonalizationHelper.currentTimeChangedForTesting(currentNativeTimestamp); + return currentNativeTimestamp; + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java b/java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java deleted file mode 100644 index ae1fd3f79..000000000 --- a/java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; - -import java.util.Collection; -import java.util.Comparator; -import java.util.TreeSet; - -/** - * A TreeSet that is bounded in size and throws everything that's smaller than its limit - */ -public final class BoundedTreeSet extends TreeSet<SuggestedWordInfo> { - private final int mCapacity; - public BoundedTreeSet(final Comparator<SuggestedWordInfo> comparator, final int capacity) { - super(comparator); - mCapacity = capacity; - } - - @Override - public boolean add(final SuggestedWordInfo e) { - if (size() < mCapacity) return super.add(e); - if (comparator().compare(e, last()) > 0) return false; - super.add(e); - pollLast(); // removes the last element - return true; - } - - @Override - public boolean addAll(final Collection<? extends SuggestedWordInfo> e) { - if (null == e) return false; - return super.addAll(e); - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java b/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java deleted file mode 100644 index 2028298f2..000000000 --- a/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2013 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.inputmethod.latin.utils; - -import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils.DictBuffer; - -/** - * This class provides an implementation for the FusionDictionary buffer interface that is backed - * by a simpled byte array. It allows to create a binary dictionary in memory. - */ -public final class ByteArrayDictBuffer implements DictBuffer { - private byte[] mBuffer; - private int mPosition; - - public ByteArrayDictBuffer(final byte[] buffer) { - mBuffer = buffer; - mPosition = 0; - } - - @Override - public int readUnsignedByte() { - return mBuffer[mPosition++] & 0xFF; - } - - @Override - public int readUnsignedShort() { - final int retval = readUnsignedByte(); - return (retval << 8) + readUnsignedByte(); - } - - @Override - public int readUnsignedInt24() { - final int retval = readUnsignedShort(); - return (retval << 8) + readUnsignedByte(); - } - - @Override - public int readInt() { - final int retval = readUnsignedShort(); - return (retval << 16) + readUnsignedShort(); - } - - @Override - public int position() { - return mPosition; - } - - @Override - public void position(int position) { - mPosition = position; - } - - @Override - public void put(final byte b) { - mBuffer[mPosition++] = b; - } - - @Override - public int limit() { - return mBuffer.length - 1; - } - - @Override - public int capacity() { - return mBuffer.length; - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java index 3d4404a98..702688f93 100644 --- a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java @@ -21,7 +21,7 @@ import android.text.TextUtils; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.WordComposer; -import com.android.inputmethod.latin.settings.SettingsValues; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import java.util.Locale; @@ -74,7 +74,7 @@ public final class CapsModeUtils { * @param reqModes The modes to be checked: may be any combination of * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and * {@link TextUtils#CAP_MODE_SENTENCES}. - * @param settingsValues The current settings values. + * @param spacingAndPunctuations The current spacing and punctuations settings. * @param hasSpaceBefore Whether we should consider there is a space inserted at the end of cs * * @return Returns the actual capitalization modes that can be in effect @@ -83,7 +83,7 @@ public final class CapsModeUtils { * {@link TextUtils#CAP_MODE_SENTENCES}. */ public static int getCapsMode(final CharSequence cs, final int reqModes, - final SettingsValues settingsValues, final boolean hasSpaceBefore) { + final SpacingAndPunctuations spacingAndPunctuations, final boolean hasSpaceBefore) { // Quick description of what we want to do: // CAP_MODE_CHARACTERS is always on. // CAP_MODE_WORDS is on if there is some whitespace before the cursor. @@ -139,6 +139,20 @@ public final class CapsModeUtils { j--; } if (j <= 0 || Character.isWhitespace(prevChar)) { + if (spacingAndPunctuations.mUsesGermanRules) { + // In German typography rules, there is a specific case that the first character + // of a new line should not be capitalized if the previous line ends in a comma. + boolean hasNewLine = false; + while (--j >= 0 && Character.isWhitespace(prevChar)) { + if (Constants.CODE_ENTER == prevChar) { + hasNewLine = true; + } + prevChar = cs.charAt(j); + } + if (Constants.CODE_COMMA == prevChar && hasNewLine) { + return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; + } + } // There are only spacing chars between the start of the paragraph and the cursor, // defined as a isWhitespace() char that is neither a isSpaceChar() nor a tab. Both // MODE_WORDS and MODE_SENTENCES should be active. @@ -167,8 +181,7 @@ public final class CapsModeUtils { // No other language has such a rule as far as I know, instead putting inside the quotation // mark as the exact thing quoted and handling the surrounding punctuation independently, // e.g. <<Did he say, "let's go home"?>> - // Hence, specifically for English, we treat this special case here. - if (Locale.ENGLISH.getLanguage().equals(settingsValues.mLocale.getLanguage())) { + if (spacingAndPunctuations.mUsesAmericanTypography) { for (; j > 0; j--) { // Here we look to go over any closing punctuation. This is because in dominant // variants of English, the final period is placed within double quotes and maybe @@ -191,7 +204,7 @@ public final class CapsModeUtils { if (c == Constants.CODE_QUESTION_MARK || c == Constants.CODE_EXCLAMATION_MARK) { return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes; } - if (settingsValues.mSentenceSeparator != c || j <= 0) { + if (!spacingAndPunctuations.isSentenceSeparator(c) || j <= 0) { return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes; } @@ -241,7 +254,7 @@ public final class CapsModeUtils { case WORD: if (Character.isLetter(c)) { state = WORD; - } else if (settingsValues.mSentenceSeparator == c) { + } else if (spacingAndPunctuations.isSentenceSeparator(c)) { state = PERIOD; } else { return caps; @@ -257,7 +270,7 @@ public final class CapsModeUtils { case LETTER: if (Character.isLetter(c)) { state = LETTER; - } else if (settingsValues.mSentenceSeparator == c) { + } else if (spacingAndPunctuations.isSentenceSeparator(c)) { state = PERIOD; } else { return noCaps; diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java index cc25102ce..bbfa0f091 100644 --- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java @@ -102,4 +102,19 @@ public final class CollectionUtils { public static <E> SparseArray<E> newSparseArray() { return new SparseArray<E>(); } + + public static <E> ArrayList<E> arrayAsList(final E[] array, final int start, final int end) { + if (array == null) { + throw new NullPointerException(); + } + if (start < 0 || start > end || end > array.length) { + throw new IllegalArgumentException(); + } + + final ArrayList<E> list = newArrayList(end - start); + for (int i = start; i < end; i++) { + list.add(array[i]); + } + return list; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java new file mode 100644 index 000000000..c66007537 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java @@ -0,0 +1,99 @@ +/* + * 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 com.android.inputmethod.latin.makedict.DictionaryHeader; +import com.android.inputmethod.latin.makedict.ProbabilityInfo; +import com.android.inputmethod.latin.makedict.WeightedString; +import com.android.inputmethod.latin.makedict.WordProperty; + +import java.util.HashMap; + +public class CombinedFormatUtils { + public static final String DICTIONARY_TAG = "dictionary"; + public static final String BIGRAM_TAG = "bigram"; + public static final String SHORTCUT_TAG = "shortcut"; + public static final String PROBABILITY_TAG = "f"; + public static final String HISTORICAL_INFO_TAG = "historicalInfo"; + public static final String HISTORICAL_INFO_SEPARATOR = ":"; + public static final String WORD_TAG = "word"; + public static final String NOT_A_WORD_TAG = "not_a_word"; + public static final String BLACKLISTED_TAG = "blacklisted"; + + public static String formatAttributeMap(final HashMap<String, String> attributeMap) { + final StringBuilder builder = new StringBuilder(); + builder.append(DICTIONARY_TAG + "="); + if (attributeMap.containsKey(DictionaryHeader.DICTIONARY_ID_KEY)) { + builder.append(attributeMap.get(DictionaryHeader.DICTIONARY_ID_KEY)); + } + for (final String key : attributeMap.keySet()) { + if (key.equals(DictionaryHeader.DICTIONARY_ID_KEY)) { + continue; + } + final String value = attributeMap.get(key); + builder.append("," + key + "=" + value); + } + builder.append("\n"); + return builder.toString(); + } + + public static String formatWordProperty(final WordProperty wordProperty) { + final StringBuilder builder = new StringBuilder(); + builder.append(" " + WORD_TAG + "=" + wordProperty.mWord); + builder.append(","); + builder.append(formatProbabilityInfo(wordProperty.mProbabilityInfo)); + if (wordProperty.mIsNotAWord) { + builder.append("," + NOT_A_WORD_TAG + "=true"); + } + if (wordProperty.mIsBlacklistEntry) { + builder.append("," + BLACKLISTED_TAG + "=true"); + } + builder.append("\n"); + if (wordProperty.mShortcutTargets != null) { + for (final WeightedString shortcutTarget : wordProperty.mShortcutTargets) { + builder.append(" " + SHORTCUT_TAG + "=" + shortcutTarget.mWord); + builder.append(","); + builder.append(formatProbabilityInfo(shortcutTarget.mProbabilityInfo)); + builder.append("\n"); + } + } + if (wordProperty.mBigrams != null) { + for (final WeightedString bigram : wordProperty.mBigrams) { + builder.append(" " + BIGRAM_TAG + "=" + bigram.mWord); + builder.append(","); + builder.append(formatProbabilityInfo(bigram.mProbabilityInfo)); + builder.append("\n"); + } + } + return builder.toString(); + } + + public static String formatProbabilityInfo(final ProbabilityInfo probabilityInfo) { + final StringBuilder builder = new StringBuilder(); + builder.append(PROBABILITY_TAG + "=" + probabilityInfo.mProbability); + if (probabilityInfo.hasHistoricalInfo()) { + builder.append(","); + builder.append(HISTORICAL_INFO_TAG + "="); + builder.append(probabilityInfo.mTimestamp); + builder.append(HISTORICAL_INFO_SEPARATOR); + builder.append(probabilityInfo.mLevel); + builder.append(HISTORICAL_INFO_SEPARATOR); + builder.append(probabilityInfo.mCount); + } + return builder.toString(); + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java b/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java index 72f2cd2d9..87df013a6 100644 --- a/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java @@ -16,17 +16,19 @@ package com.android.inputmethod.latin.utils; +import java.util.Arrays; + public final class CoordinateUtils { private static final int INDEX_X = 0; private static final int INDEX_Y = 1; - private static final int ARRAY_SIZE = INDEX_Y + 1; + private static final int ELEMENT_SIZE = INDEX_Y + 1; private CoordinateUtils() { // This utility class is not publicly instantiable. } public static int[] newInstance() { - return new int[ARRAY_SIZE]; + return new int[ELEMENT_SIZE]; } public static int x(final int[] coords) { @@ -46,4 +48,44 @@ public final class CoordinateUtils { destination[INDEX_X] = source[INDEX_X]; destination[INDEX_Y] = source[INDEX_Y]; } + + public static int[] newCoordinateArray(final int arraySize) { + return new int[ELEMENT_SIZE * arraySize]; + } + + public static int[] newCoordinateArray(final int arraySize, + final int defaultX, final int defaultY) { + final int[] result = new int[ELEMENT_SIZE * arraySize]; + for (int i = 0; i < arraySize; ++i) { + setXYInArray(result, i, defaultX, defaultY); + } + return result; + } + + public static int xFromArray(final int[] coordsArray, final int index) { + return coordsArray[ELEMENT_SIZE * index + INDEX_X]; + } + + public static int yFromArray(final int[] coordsArray, final int index) { + return coordsArray[ELEMENT_SIZE * index + INDEX_Y]; + } + + public static int[] coordinateFromArray(final int[] coordsArray, final int index) { + final int baseIndex = ELEMENT_SIZE * index; + return Arrays.copyOfRange(coordsArray, baseIndex, baseIndex + ELEMENT_SIZE); + } + + public static void setXYInArray(final int[] coordsArray, final int index, + final int x, final int y) { + final int baseIndex = ELEMENT_SIZE * index; + coordsArray[baseIndex + INDEX_X] = x; + coordsArray[baseIndex + INDEX_Y] = y; + } + + public static void setCoordinateInArray(final int[] coordsArray, final int index, + final int[] coords) { + final int baseIndex = ELEMENT_SIZE * index; + coordsArray[baseIndex + INDEX_X] = coords[INDEX_X]; + coordsArray[baseIndex + INDEX_Y] = coords[INDEX_Y]; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java index 36b927eea..b18a1d83b 100644 --- a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java @@ -17,6 +17,7 @@ package com.android.inputmethod.latin.utils; import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.Constants; import java.util.ArrayList; @@ -57,9 +58,9 @@ public final class CsvUtils { // Note that none of these characters match high or low surrogate characters, so we need not // take care of matching by code point. - private static final char COMMA = ','; - private static final char SPACE = ' '; - private static final char QUOTE = '"'; + private static final char COMMA = Constants.CODE_COMMA; + private static final char SPACE = Constants.CODE_SPACE; + private static final char QUOTE = Constants.CODE_DOUBLE_QUOTE; @SuppressWarnings("serial") public static class CsvParseException extends RuntimeException { diff --git a/java/src/com/android/inputmethod/latin/utils/DialogUtils.java b/java/src/com/android/inputmethod/latin/utils/DialogUtils.java new file mode 100644 index 000000000..a05c932d0 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/DialogUtils.java @@ -0,0 +1,34 @@ +/* + * 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.content.Context; +import android.view.ContextThemeWrapper; + +import com.android.inputmethod.latin.R; + +public final class DialogUtils { + private DialogUtils() { + // This utility class is not publicly instantiable. + } + + public static Context getPlatformDialogThemeContext(final Context context) { + // Because {@link AlertDialog.Builder.create()} doesn't honor the specified theme with + // createThemeContextWrapper=false, the result dialog box has unneeded paddings around it. + return new ContextThemeWrapper(context, R.style.platformDialogTheme); + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java index 021bf0825..315913e2f 100644 --- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java @@ -20,15 +20,19 @@ import android.content.ContentValues; import android.content.Context; import android.content.res.AssetManager; import android.content.res.Resources; +import android.text.TextUtils; import android.util.Log; import com.android.inputmethod.latin.AssetFileAddress; import com.android.inputmethod.latin.BinaryDictionaryGetter; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; -import com.android.inputmethod.latin.makedict.BinaryDictIOUtils; -import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader; +import com.android.inputmethod.latin.makedict.DictionaryHeader; +import com.android.inputmethod.latin.makedict.UnsupportedFormatException; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.Locale; @@ -278,14 +282,36 @@ public class DictionaryInfoUtils { BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.getLanguage().toString(); } - public static FileHeader getDictionaryFileHeaderOrNull(final File file) { - return BinaryDictIOUtils.getDictionaryFileHeaderOrNull(file, 0, file.length()); + public static DictionaryHeader getDictionaryFileHeaderOrNull(final File file) { + return getDictionaryFileHeaderOrNull(file, 0, file.length()); } + private static DictionaryHeader getDictionaryFileHeaderOrNull(final File file, + final long offset, final long length) { + try { + final DictionaryHeader header = + BinaryDictionaryUtils.getHeaderWithOffsetAndLength(file, offset, length); + return header; + } catch (UnsupportedFormatException e) { + return null; + } catch (IOException e) { + return null; + } + } + + /** + * Returns information of the dictionary. + * + * @param fileAddress the asset dictionary file address. + * @return information of the specified dictionary. + */ private static DictionaryInfo createDictionaryInfoFromFileAddress( final AssetFileAddress fileAddress) { - final FileHeader header = BinaryDictIOUtils.getDictionaryFileHeaderOrNull( + final DictionaryHeader header = getDictionaryFileHeaderOrNull( new File(fileAddress.mFilename), fileAddress.mOffset, fileAddress.mLength); + if (header == null) { + return null; + } final String id = header.getId(); final Locale locale = LocaleUtils.constructLocaleFromString(header.getLocaleString()); final String description = header.getDescription(); @@ -328,7 +354,7 @@ public class DictionaryInfoUtils { // Protect against cases of a less-specific dictionary being found, like an // en dictionary being used for an en_US locale. In this case, the en dictionary // should be used for en_US but discounted for listing purposes. - if (!dictionaryInfo.mLocale.equals(locale)) continue; + if (dictionaryInfo == null || !dictionaryInfo.mLocale.equals(locale)) continue; addOrUpdateDictInfo(dictList, dictionaryInfo); } } @@ -355,4 +381,32 @@ public class DictionaryInfoUtils { return dictList; } + + public static boolean looksValidForDictionaryInsertion(final CharSequence text, + final SpacingAndPunctuations spacingAndPunctuations) { + if (TextUtils.isEmpty(text)) return false; + final int length = text.length(); + // TODO: Make this test "length > Constants.DICTIONARY_MAX_WORD_LENGTH". + if (length >= Constants.DICTIONARY_MAX_WORD_LENGTH) { + return false; + } + int i = 0; + int digitCount = 0; + while (i < length) { + final int codePoint = Character.codePointAt(text, i); + final int charCount = Character.charCount(codePoint); + i += charCount; + if (Character.isDigit(codePoint)) { + // Count digits: see below + digitCount += charCount; + continue; + } + if (!spacingAndPunctuations.isWordCodePoint(codePoint)) return false; + } + // We reject strings entirely comprised of digits to avoid using PIN codes or credit + // card numbers. It would come in handy for word prediction though; a good example is + // when writing one's address where the street number is usually quite discriminative, + // as well as the postal code. + return digitCount < length; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java new file mode 100644 index 000000000..ed502ed3d --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java @@ -0,0 +1,60 @@ +/* + * 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 com.android.inputmethod.annotations.UsedForTesting; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Utilities to manage executors. + */ +public class ExecutorUtils { + private static final ConcurrentHashMap<String, PrioritizedSerialExecutor> + sExecutorMap = CollectionUtils.newConcurrentHashMap(); + /** + * Gets the executor for the given dictionary name. + */ + public static PrioritizedSerialExecutor getExecutor(final String dictName) { + PrioritizedSerialExecutor executor = sExecutorMap.get(dictName); + if (executor == null) { + synchronized(sExecutorMap) { + executor = new PrioritizedSerialExecutor(); + sExecutorMap.put(dictName, executor); + } + } + return executor; + } + + /** + * Shutdowns all executors and removes all executors from the executor map for testing. + */ + @UsedForTesting + public static void shutdownAllExecutors() { + synchronized(sExecutorMap) { + for (final PrioritizedSerialExecutor executor : sExecutorMap.values()) { + executor.execute(new Runnable() { + @Override + public void run() { + executor.shutdown(); + sExecutorMap.remove(executor); + } + }); + } + } + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/FileUtils.java b/java/src/com/android/inputmethod/latin/utils/FileUtils.java new file mode 100644 index 000000000..f1106a6c6 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/FileUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin.utils; + +import java.io.File; +import java.io.FilenameFilter; + +/** + * A simple class to help with removing directories recursively. + */ +public class FileUtils { + public static boolean deleteRecursively(final File path) { + if (path.isDirectory()) { + final File[] files = path.listFiles(); + if (files != null) { + for (final File child : files) { + deleteRecursively(child); + } + } + } + return path.delete(); + } + + public static boolean deleteFilteredFiles(final File dir, final FilenameFilter fileNameFilter) { + if (!dir.isDirectory()) { + return false; + } + final File[] files = dir.listFiles(fileNameFilter); + if (files == null) { + return false; + } + boolean hasDeletedAllFiles = true; + for (final File file : files) { + if (!deleteRecursively(file)) { + hasDeletedAllFiles = false; + } + } + return hasDeletedAllFiles; + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java new file mode 100644 index 000000000..7d937a9d2 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/ImportantNoticeUtils.java @@ -0,0 +1,125 @@ +/* + * 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.content.Context; +import android.content.SharedPreferences; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.text.TextUtils; +import android.util.Log; + +import com.android.inputmethod.latin.InputAttributes; +import com.android.inputmethod.latin.R; + +public final class ImportantNoticeUtils { + private static final String TAG = ImportantNoticeUtils.class.getSimpleName(); + + // {@link SharedPreferences} name to save the last important notice version that has been + // displayed to users. + private static final String PREFERENCE_NAME = "important_notice_pref"; + private static final String KEY_IMPORTANT_NOTICE_VERSION = "important_notice_version"; + public static final int VERSION_TO_ENABLE_PERSONALIZED_SUGGESTIONS = 1; + + // Copy of the hidden {@link Settings.Secure#USER_SETUP_COMPLETE} settings key. + // The value is zero until each multiuser completes system setup wizard. + // Caveat: This is a hidden API. + private static final String Settings_Secure_USER_SETUP_COMPLETE = "user_setup_complete"; + private static final int USER_SETUP_IS_NOT_COMPLETE = 0; + + private ImportantNoticeUtils() { + // This utility class is not publicly instantiable. + } + + private static boolean isInSystemSetupWizard(final Context context) { + try { + final int userSetupComplete = Settings.Secure.getInt( + context.getContentResolver(), Settings_Secure_USER_SETUP_COMPLETE); + return userSetupComplete == USER_SETUP_IS_NOT_COMPLETE; + } catch (final SettingNotFoundException e) { + Log.w(TAG, "Can't find settings in Settings.Secure: key=" + + Settings_Secure_USER_SETUP_COMPLETE); + return false; + } + } + + private static SharedPreferences getImportantNoticePreferences(final Context context) { + return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE); + } + + private static int getCurrentImportantNoticeVersion(final Context context) { + return context.getResources().getInteger(R.integer.config_important_notice_version); + } + + private static int getLastImportantNoticeVersion(final Context context) { + return getImportantNoticePreferences(context).getInt(KEY_IMPORTANT_NOTICE_VERSION, 0); + } + + public static int getNextImportantNoticeVersion(final Context context) { + return getLastImportantNoticeVersion(context) + 1; + } + + private static boolean hasNewImportantNotice(final Context context) { + final int lastVersion = getLastImportantNoticeVersion(context); + return getCurrentImportantNoticeVersion(context) > lastVersion; + } + + public static boolean shouldShowImportantNotice(final Context context, + final InputAttributes inputAttributes) { + if (inputAttributes == null || inputAttributes.mIsPasswordField) { + return false; + } + if (isInSystemSetupWizard(context)) { + return false; + } + if (!hasNewImportantNotice(context)) { + return false; + } + final String importantNoticeTitle = getNextImportantNoticeTitle(context); + if (TextUtils.isEmpty(importantNoticeTitle)) { + return false; + } + return true; + } + + public static void updateLastImportantNoticeVersion(final Context context) { + getImportantNoticePreferences(context) + .edit() + .putInt(KEY_IMPORTANT_NOTICE_VERSION, getNextImportantNoticeVersion(context)) + .apply(); + } + + public static String getNextImportantNoticeTitle(final Context context) { + final int nextVersion = getCurrentImportantNoticeVersion(context); + final String[] importantNoticeTitleArray = context.getResources().getStringArray( + R.array.important_notice_title_array); + if (nextVersion > 0 && nextVersion < importantNoticeTitleArray.length) { + return importantNoticeTitleArray[nextVersion]; + } + return null; + } + + public static String getNextImportantNoticeContents(final Context context) { + final int nextVersion = getNextImportantNoticeVersion(context); + final String[] importantNoticeContentsArray = context.getResources().getStringArray( + R.array.important_notice_contents_array); + if (nextVersion > 0 && nextVersion < importantNoticeContentsArray.length) { + return importantNoticeContentsArray[nextVersion]; + } + return null; + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/JsonUtils.java b/java/src/com/android/inputmethod/latin/utils/JsonUtils.java new file mode 100644 index 000000000..764ef72ce --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/JsonUtils.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.latin.utils; + +import android.util.JsonReader; +import android.util.JsonWriter; +import android.util.Log; + +import java.io.Closeable; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class JsonUtils { + private static final String TAG = JsonUtils.class.getSimpleName(); + + private static final String INTEGER_CLASS_NAME = Integer.class.getSimpleName(); + private static final String STRING_CLASS_NAME = String.class.getSimpleName(); + + private static final String EMPTY_STRING = ""; + + public static List<Object> jsonStrToList(final String s) { + final ArrayList<Object> list = CollectionUtils.newArrayList(); + final JsonReader reader = new JsonReader(new StringReader(s)); + try { + reader.beginArray(); + while (reader.hasNext()) { + reader.beginObject(); + while (reader.hasNext()) { + final String name = reader.nextName(); + if (name.equals(INTEGER_CLASS_NAME)) { + list.add(reader.nextInt()); + } else if (name.equals(STRING_CLASS_NAME)) { + list.add(reader.nextString()); + } else { + Log.w(TAG, "Invalid name: " + name); + reader.skipValue(); + } + } + reader.endObject(); + } + reader.endArray(); + return list; + } catch (final IOException e) { + } finally { + close(reader); + } + return Collections.<Object>emptyList(); + } + + public static String listToJsonStr(final List<Object> list) { + if (list == null || list.isEmpty()) { + return EMPTY_STRING; + } + final StringWriter sw = new StringWriter(); + final JsonWriter writer = new JsonWriter(sw); + try { + writer.beginArray(); + for (final Object o : list) { + writer.beginObject(); + if (o instanceof Integer) { + writer.name(INTEGER_CLASS_NAME).value((Integer)o); + } else if (o instanceof String) { + writer.name(STRING_CLASS_NAME).value((String)o); + } + writer.endObject(); + } + writer.endArray(); + return sw.toString(); + } catch (final IOException e) { + } finally { + close(writer); + } + return EMPTY_STRING; + } + + private static void close(final Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (final IOException e) { + // Ignore + } + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java new file mode 100644 index 000000000..5ce977d5e --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java @@ -0,0 +1,177 @@ +/* + * 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.util.Log; + +import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest; +import com.android.inputmethod.latin.settings.SpacingAndPunctuations; + +import java.util.ArrayList; +import java.util.Locale; + +// Note: this class is used as a parameter type of a native method. You should be careful when you +// rename this class or field name. See BinaryDictionary#addMultipleDictionaryEntriesNative(). +public final class LanguageModelParam { + private static final String TAG = LanguageModelParam.class.getSimpleName(); + private static final boolean DEBUG = false; + private static final boolean DEBUG_TOKEN = false; + + // For now, these probability values are being referred to only when we add new entries to + // decaying dynamic binary dictionaries. When these are referred to, what matters is 0 or + // non-0. Thus, it's not meaningful to compare 10, 100, and so on. + // TODO: Revise the logic in ForgettingCurveUtils in native code. + private static final int UNIGRAM_PROBABILITY_FOR_VALID_WORD = 100; + private static final int UNIGRAM_PROBABILITY_FOR_OOV_WORD = Dictionary.NOT_A_PROBABILITY; + private static final int BIGRAM_PROBABILITY_FOR_VALID_WORD = 10; + private static final int BIGRAM_PROBABILITY_FOR_OOV_WORD = Dictionary.NOT_A_PROBABILITY; + + public final String mTargetWord; + public final int[] mWord0; + public final int[] mWord1; + // TODO: this needs to be a list of shortcuts + public final int[] mShortcutTarget; + public final int mUnigramProbability; + public final int mBigramProbability; + public final int mShortcutProbability; + public final boolean mIsNotAWord; + public final boolean mIsBlacklisted; + // Time stamp in seconds. + public final int mTimestamp; + + // Constructor for unigram. TODO: support shortcuts + public LanguageModelParam(final String word, final int unigramProbability, + final int timestamp) { + this(null /* word0 */, word, unigramProbability, Dictionary.NOT_A_PROBABILITY, timestamp); + } + + // Constructor for unigram and bigram. + public LanguageModelParam(final String word0, final String word1, + final int unigramProbability, final int bigramProbability, + final int timestamp) { + mTargetWord = word1; + mWord0 = (word0 == null) ? null : StringUtils.toCodePointArray(word0); + mWord1 = StringUtils.toCodePointArray(word1); + mShortcutTarget = null; + mUnigramProbability = unigramProbability; + mBigramProbability = bigramProbability; + mShortcutProbability = Dictionary.NOT_A_PROBABILITY; + mIsNotAWord = false; + mIsBlacklisted = false; + mTimestamp = timestamp; + } + + // Process a list of words and return a list of {@link LanguageModelParam} objects. + public static ArrayList<LanguageModelParam> createLanguageModelParamsFrom( + final ArrayList<String> tokens, final int timestamp, + final DictionaryFacilitatorForSuggest dictionaryFacilitator, + final SpacingAndPunctuations spacingAndPunctuations) { + final ArrayList<LanguageModelParam> languageModelParams = + CollectionUtils.newArrayList(); + final int N = tokens.size(); + String prevWord = null; + for (int i = 0; i < N; ++i) { + final String tempWord = tokens.get(i); + if (StringUtils.isEmptyStringOrWhiteSpaces(tempWord)) { + // just skip this token + if (DEBUG_TOKEN) { + Log.d(TAG, "--- isEmptyStringOrWhiteSpaces: \"" + tempWord + "\""); + } + continue; + } + if (!DictionaryInfoUtils.looksValidForDictionaryInsertion( + tempWord, spacingAndPunctuations)) { + if (DEBUG_TOKEN) { + Log.d(TAG, "--- not looksValidForDictionaryInsertion: \"" + + tempWord + "\""); + } + // Sentence terminator found. Split. + prevWord = null; + continue; + } + if (DEBUG_TOKEN) { + Log.d(TAG, "--- word: \"" + tempWord + "\""); + } + final LanguageModelParam languageModelParam = + detectWhetherVaildWordOrNotAndGetLanguageModelParam( + prevWord, tempWord, timestamp, dictionaryFacilitator); + if (languageModelParam == null) { + continue; + } + languageModelParams.add(languageModelParam); + prevWord = languageModelParam.mTargetWord; + } + return languageModelParams; + } + + private static LanguageModelParam detectWhetherVaildWordOrNotAndGetLanguageModelParam( + final String prevWord, final String targetWord, final int timestamp, + final DictionaryFacilitatorForSuggest dictionaryFacilitator) { + final Locale locale = dictionaryFacilitator.getLocale(); + if (locale == null) { + return null; + } + if (!dictionaryFacilitator.isValidWord(targetWord, true /* ignoreCase */)) { + // OOV word. + return createAndGetLanguageModelParamOfWord(prevWord, targetWord, timestamp, + false /* isValidWord */, locale); + } + if (dictionaryFacilitator.isValidWord(targetWord, false /* ignoreCase */)) { + return createAndGetLanguageModelParamOfWord(prevWord, targetWord, timestamp, + true /* isValidWord */, locale); + } + final String lowerCaseTargetWord = targetWord.toLowerCase(locale); + if (dictionaryFacilitator.isValidWord(lowerCaseTargetWord, false /* ignoreCase */)) { + // Add the lower-cased word. + return createAndGetLanguageModelParamOfWord(prevWord, lowerCaseTargetWord, + timestamp, true /* isValidWord */, locale); + } + // Treat the word as an OOV word. + return createAndGetLanguageModelParamOfWord(prevWord, targetWord, timestamp, + false /* isValidWord */, locale); + } + + private static LanguageModelParam createAndGetLanguageModelParamOfWord( + final String prevWord, final String targetWord, final int timestamp, + final boolean isValidWord, final Locale locale) { + final String word; + if (StringUtils.getCapitalizationType(targetWord) == StringUtils.CAPITALIZE_FIRST + && prevWord == null && !isValidWord) { + word = targetWord.toLowerCase(locale); + } else { + word = targetWord; + } + final int unigramProbability = isValidWord ? + UNIGRAM_PROBABILITY_FOR_VALID_WORD : UNIGRAM_PROBABILITY_FOR_OOV_WORD; + if (prevWord == null) { + if (DEBUG) { + Log.d(TAG, "--- add unigram: current(" + + (isValidWord ? "Valid" : "OOV") + ") = " + word); + } + return new LanguageModelParam(word, unigramProbability, timestamp); + } + if (DEBUG) { + Log.d(TAG, "--- add bigram: prev = " + prevWord + ", current(" + + (isValidWord ? "Valid" : "OOV") + ") = " + word); + } + final int bigramProbability = isValidWord ? + BIGRAM_PROBABILITY_FOR_VALID_WORD : BIGRAM_PROBABILITY_FOR_OOV_WORD; + return new LanguageModelParam(prevWord, word, unigramProbability, + bigramProbability, timestamp); + } +} diff --git a/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java b/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java index e958a7e71..d14ba508b 100644 --- a/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java @@ -35,7 +35,7 @@ public final class LatinImeLoggerUtils { public static void onSeparator(final int code, final int x, final int y) { // Helper method to log a single code point separator // TODO: cache this mapping of a code point to a string in a sparse array in StringUtils - onSeparator(new String(new int[]{code}, 0, 1), x, y); + onSeparator(StringUtils.newSingleCodePointString(code), x, y); } public static void onSeparator(final String separator, final int x, final int y) { diff --git a/java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java b/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java index 44e5d17b4..8469c87b0 100644 --- a/java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java +++ b/java/src/com/android/inputmethod/latin/utils/LeakGuardHandlerWrapper.java @@ -21,22 +21,22 @@ import android.os.Looper; import java.lang.ref.WeakReference; -public class StaticInnerHandlerWrapper<T> extends Handler { - private final WeakReference<T> mOuterInstanceRef; +public class LeakGuardHandlerWrapper<T> extends Handler { + private final WeakReference<T> mOwnerInstanceRef; - public StaticInnerHandlerWrapper(final T outerInstance) { - this(outerInstance, Looper.myLooper()); + public LeakGuardHandlerWrapper(final T ownerInstance) { + this(ownerInstance, Looper.myLooper()); } - public StaticInnerHandlerWrapper(final T outerInstance, final Looper looper) { + public LeakGuardHandlerWrapper(final T ownerInstance, final Looper looper) { super(looper); - if (outerInstance == null) { - throw new NullPointerException("outerInstance is null"); + if (ownerInstance == null) { + throw new NullPointerException("ownerInstance is null"); } - mOuterInstanceRef = new WeakReference<T>(outerInstance); + mOwnerInstanceRef = new WeakReference<T>(ownerInstance); } - public T getOuterInstance() { - return mOuterInstanceRef.get(); + public T getOwnerInstance() { + return mOwnerInstanceRef.get(); } } diff --git a/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java index 22045aa38..0c55484b4 100644 --- a/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java @@ -30,9 +30,6 @@ import java.util.Locale; * dictionary pack. */ public final class LocaleUtils { - private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap(); - private static final String LOCALE_AND_TIME_STR_SEPARATER = ","; - private LocaleUtils() { // Intentional empty constructor for utility class. } @@ -168,12 +165,14 @@ public final class LocaleUtils { * Creates a locale from a string specification. */ public static Locale constructLocaleFromString(final String localeStr) { - if (localeStr == null) + if (localeStr == null) { return null; + } synchronized (sLocaleCache) { - if (sLocaleCache.containsKey(localeStr)) - return sLocaleCache.get(localeStr); - Locale retval = null; + Locale retval = sLocaleCache.get(localeStr); + if (retval != null) { + return retval; + } String[] localeParams = localeStr.split("_", 3); if (localeParams.length == 1) { retval = new Locale(localeParams[0]); @@ -188,38 +187,4 @@ public final class LocaleUtils { return retval; } } - - public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) { - if (TextUtils.isEmpty(str)) { - return EMPTY_LT_HASH_MAP; - } - final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER); - final int N = ss.length; - if (N < 2 || N % 2 != 0) { - return EMPTY_LT_HASH_MAP; - } - final HashMap<String, Long> retval = CollectionUtils.newHashMap(); - for (int i = 0; i < N / 2; ++i) { - final String localeStr = ss[i * 2]; - final long time = Long.valueOf(ss[i * 2 + 1]); - retval.put(localeStr, time); - } - return retval; - } - - public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) { - if (map == null || map.isEmpty()) { - return ""; - } - final StringBuilder builder = new StringBuilder(); - for (String localeStr : map.keySet()) { - if (builder.length() > 0) { - builder.append(LOCALE_AND_TIME_STR_SEPARATER); - } - final Long time = map.get(localeStr); - builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER); - builder.append(String.valueOf(time)); - } - return builder.toString(); - } } diff --git a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java index 201a70d42..b10d08af3 100644 --- a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java +++ b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java @@ -137,6 +137,7 @@ public class PrioritizedSerialExecutor { public void shutdown() { synchronized(mLock) { mIsShutdown = true; + mThreadPoolExecutor.shutdown(); } } diff --git a/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java index 0f5cd80db..4521ec531 100644 --- a/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java +++ b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java @@ -37,12 +37,12 @@ public class RecapitalizeStatus { CAPS_MODE_ALL_UPPER }; - private static final int getStringMode(final String string, final String separators) { + private static final int getStringMode(final String string, final int[] sortedSeparators) { if (StringUtils.isIdenticalAfterUpcase(string)) { return CAPS_MODE_ALL_UPPER; } else if (StringUtils.isIdenticalAfterDowncase(string)) { return CAPS_MODE_ALL_LOWER; - } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, separators)) { + } else if (StringUtils.isIdenticalAfterCapitalizeEachWord(string, sortedSeparators)) { return CAPS_MODE_FIRST_WORD_UPPER; } else { return CAPS_MODE_ORIGINAL_MIXED_CASE; @@ -60,26 +60,28 @@ public class RecapitalizeStatus { private int mRotationStyleCurrentIndex; private boolean mSkipOriginalMixedCaseMode; private Locale mLocale; - private String mSeparators; + private int[] mSortedSeparators; private String mStringAfter; private boolean mIsActive; + private static final int[] EMPTY_STORTED_SEPARATORS = {}; + public RecapitalizeStatus() { // By default, initialize with dummy values that won't match any real recapitalize. - initialize(-1, -1, "", Locale.getDefault(), ""); + initialize(-1, -1, "", Locale.getDefault(), EMPTY_STORTED_SEPARATORS); deactivate(); } public void initialize(final int cursorStart, final int cursorEnd, final String string, - final Locale locale, final String separators) { + final Locale locale, final int[] sortedSeparators) { mCursorStartBefore = cursorStart; mStringBefore = string; mCursorStartAfter = cursorStart; mCursorEndAfter = cursorEnd; mStringAfter = string; - final int initialMode = getStringMode(mStringBefore, separators); + final int initialMode = getStringMode(mStringBefore, sortedSeparators); mLocale = locale; - mSeparators = separators; + mSortedSeparators = sortedSeparators; if (CAPS_MODE_ORIGINAL_MIXED_CASE == initialMode) { mRotationStyleCurrentIndex = 0; mSkipOriginalMixedCaseMode = false; @@ -131,7 +133,7 @@ public class RecapitalizeStatus { mStringAfter = mStringBefore.toLowerCase(mLocale); break; case CAPS_MODE_FIRST_WORD_UPPER: - mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSeparators, + mStringAfter = StringUtils.capitalizeEachWord(mStringBefore, mSortedSeparators, mLocale); break; case CAPS_MODE_ALL_UPPER: diff --git a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java index 7c6fe93ac..64c9e2cff 100644 --- a/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java +++ b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java @@ -34,7 +34,7 @@ public final class ResizableIntArray { throw new ArrayIndexOutOfBoundsException("length=" + mLength + "; index=" + index); } - public void add(final int index, final int val) { + public void addAt(final int index, final int val) { if (index < mLength) { mArray[index] = val; } else { diff --git a/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java index 22c92446a..49f4929b4 100644 --- a/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java @@ -67,7 +67,8 @@ public final class ResourceUtils { sBuildKeyValuesDebugString = "[" + TextUtils.join(" ", keyValuePairs) + "]"; } - public static String getDeviceOverrideValue(final Resources res, final int overrideResId) { + public static String getDeviceOverrideValue(final Resources res, final int overrideResId, + final String defaultValue) { final int orientation = res.getConfiguration().orientation; final String key = overrideResId + "-" + orientation; if (sDeviceOverrideValueMap.containsKey(key)) { @@ -86,23 +87,6 @@ public final class ResourceUtils { return overrideValue; } - String defaultValue = null; - try { - defaultValue = findDefaultConstant(overrideArray); - // The defaultValue might be an empty string. - if (defaultValue == null) { - Log.w(TAG, "Couldn't find override value nor default value:" - + " resource="+ res.getResourceEntryName(overrideResId) - + " build=" + sBuildKeyValuesDebugString); - } else { - Log.i(TAG, "Found default value:" - + " resource="+ res.getResourceEntryName(overrideResId) - + " build=" + sBuildKeyValuesDebugString - + " default=" + defaultValue); - } - } catch (final DeviceOverridePatternSyntaxError e) { - Log.w(TAG, "Syntax error, ignored", e); - } sDeviceOverrideValueMap.put(key, defaultValue); return defaultValue; } @@ -152,8 +136,7 @@ public final class ResourceUtils { } final String condition = conditionConstant.substring(0, posComma); if (condition.isEmpty()) { - // Default condition. The default condition should be searched by - // {@link #findConstantForDefault(String[])}. + Log.w(TAG, "Array element has no condition: " + conditionConstant); continue; } try { @@ -199,24 +182,6 @@ public final class ResourceUtils { return matchedAll; } - @UsedForTesting - static String findDefaultConstant(final String[] conditionConstantArray) - throws DeviceOverridePatternSyntaxError { - if (conditionConstantArray == null) { - return null; - } - for (final String condition : conditionConstantArray) { - final int posComma = condition.indexOf(','); - if (posComma < 0) { - throw new DeviceOverridePatternSyntaxError("Array element has no comma", condition); - } - if (posComma == 0) { // condition is empty. - return condition.substring(posComma + 1); - } - } - return null; - } - public static int getDefaultKeyboardWidth(final Resources res) { final DisplayMetrics dm = res.getDisplayMetrics(); return dm.widthPixels; @@ -224,22 +189,23 @@ public final class ResourceUtils { public static int getDefaultKeyboardHeight(final Resources res) { final DisplayMetrics dm = res.getDisplayMetrics(); - final String keyboardHeightString = getDeviceOverrideValue(res, R.array.keyboard_heights); + final String keyboardHeightInDp = getDeviceOverrideValue( + res, R.array.keyboard_heights, null /* defaultValue */); final float keyboardHeight; - if (TextUtils.isEmpty(keyboardHeightString)) { - keyboardHeight = res.getDimension(R.dimen.keyboardHeight); + if (TextUtils.isEmpty(keyboardHeightInDp)) { + keyboardHeight = res.getDimension(R.dimen.config_default_keyboard_height); } else { - keyboardHeight = Float.parseFloat(keyboardHeightString) * dm.density; + keyboardHeight = Float.parseFloat(keyboardHeightInDp) * dm.density; } final float maxKeyboardHeight = res.getFraction( - R.fraction.maxKeyboardHeight, dm.heightPixels, dm.heightPixels); + R.fraction.config_max_keyboard_height, dm.heightPixels, dm.heightPixels); float minKeyboardHeight = res.getFraction( - R.fraction.minKeyboardHeight, dm.heightPixels, dm.heightPixels); + R.fraction.config_min_keyboard_height, dm.heightPixels, dm.heightPixels); if (minKeyboardHeight < 0.0f) { // Specified fraction was negative, so it should be calculated against display // width. minKeyboardHeight = -res.getFraction( - R.fraction.minKeyboardHeight, dm.widthPixels, dm.widthPixels); + R.fraction.config_min_keyboard_height, dm.widthPixels, dm.widthPixels); } // Keyboard height will not exceed maxKeyboardHeight and will not be less than // minKeyboardHeight. @@ -260,6 +226,10 @@ public final class ResourceUtils { return dimension >= 0; } + public static float getFloatFromFraction(final Resources res, final int fractionResId) { + return res.getFraction(fractionResId, 1, 1); + } + public static float getFraction(final TypedArray a, final int index, final float defValue) { final TypedValue value = a.peekValue(index); if (value == null || !isFractionValue(value)) { diff --git a/java/src/com/android/inputmethod/latin/utils/RunInLocale.java b/java/src/com/android/inputmethod/latin/utils/RunInLocale.java index 2c9e3b191..3c632bbc3 100644 --- a/java/src/com/android/inputmethod/latin/utils/RunInLocale.java +++ b/java/src/com/android/inputmethod/latin/utils/RunInLocale.java @@ -30,25 +30,23 @@ public abstract class RunInLocale<T> { * Execute {@link #job(Resources)} method in specified system locale exclusively. * * @param res the resources to use. - * @param newLocale the locale to change to. + * @param newLocale the locale to change to. Run in system locale if null. * @return the value returned from {@link #job(Resources)}. */ public T runInLocale(final Resources res, final Locale newLocale) { synchronized (sLockForRunInLocale) { - final Configuration conf = res.getConfiguration(); - final Locale oldLocale = conf.locale; - final boolean needsChange = (newLocale != null && !newLocale.equals(oldLocale)); + final Configuration savedConf = res.getConfiguration(); + if (newLocale == null || newLocale.equals(savedConf.locale)) { + return job(res); + } + final Configuration newConf = new Configuration(); + newConf.setTo(savedConf); + newConf.setLocale(newLocale); try { - if (needsChange) { - conf.locale = newLocale; - res.updateConfiguration(conf, null); - } + res.updateConfiguration(newConf, null); return job(res); } finally { - if (needsChange) { - conf.locale = oldLocale; - res.updateConfiguration(conf, null); - } + res.updateConfiguration(savedConf, null); } } } diff --git a/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java b/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java new file mode 100644 index 000000000..1ca895fdb --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java @@ -0,0 +1,58 @@ +/* + * 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/SpannableStringUtils.java b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java index b51fd9377..38164cb36 100644 --- a/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java @@ -22,6 +22,7 @@ import android.text.Spanned; import android.text.SpannedString; import android.text.TextUtils; import android.text.style.SuggestionSpan; +import android.text.style.URLSpan; public final class SpannableStringUtils { /** @@ -40,12 +41,17 @@ public final class SpannableStringUtils { * are out of range in <code>dest</code>. */ public static void copyNonParagraphSuggestionSpansFrom(Spanned source, int start, int end, - Spannable dest, int destoff) { + Spannable dest, int destoff) { Object[] spans = source.getSpans(start, end, SuggestionSpan.class); for (int i = 0; i < spans.length; i++) { int fl = source.getSpanFlags(spans[i]); - if (0 != (fl & Spannable.SPAN_PARAGRAPH)) continue; + // We don't care about the PARAGRAPH flag in LatinIME code. However, if this flag + // is set, Spannable#setSpan will throw an exception unless the span is on the edge + // of a word. But the spans have been split into two by the getText{Before,After}Cursor + // methods, so after concatenation they may end in the middle of a word. + // Since we don't use them, we can just remove them and avoid crashing. + fl &= ~Spannable.SPAN_PARAGRAPH; int st = source.getSpanStart(spans[i]); int en = source.getSpanEnd(spans[i]); @@ -107,4 +113,16 @@ public final class SpannableStringUtils { return new SpannedString(ss); } + + public static boolean hasUrlSpans(final CharSequence text, + final int startIndex, final int endIndex) { + if (!(text instanceof Spanned)) { + return false; // Not spanned, so no link + } + final Spanned spanned = (Spanned)text; + // getSpans(x, y) does not return spans that start on x or end on y. x-1, y+1 does the + // trick, and works in all cases even if startIndex <= 0 or endIndex >= text.length(). + final URLSpan[] spans = spanned.getSpans(startIndex - 1, endIndex + 1, URLSpan.class); + return null != spans && spans.length > 0; + } } diff --git a/java/src/com/android/inputmethod/latin/utils/StatsUtils.java b/java/src/com/android/inputmethod/latin/utils/StatsUtils.java new file mode 100644 index 000000000..a059f877b --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/StatsUtils.java @@ -0,0 +1,53 @@ +/* + * 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.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.android.inputmethod.latin.settings.Settings; + +public final class StatsUtils { + private static final String TAG = StatsUtils.class.getSimpleName(); + private static final StatsUtils sInstance = new StatsUtils(); + + public static void onCreateCompleted(final Context context) { + sInstance.onCreateCompletedInternal(context); + } + + private void onCreateCompletedInternal(final Context context) { + mContext = context; + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); + final Boolean usePersonalizedDict = + prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true); + Log.d(TAG, "onCreateCompleted. context: " + context.toString() + "usePersonalizedDict: " + + usePersonalizedDict); + } + + public static void onDestroy() { + sInstance.onDestroyInternal(); + } + + private void onDestroyInternal() { + Log.d(TAG, "onDestroy. context: " + mContext.toString()); + mContext = null; + } + + private Context mContext; +} diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java index a36548392..374badc19 100644 --- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java @@ -16,21 +16,15 @@ package com.android.inputmethod.latin.utils; -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.settings.SettingsValues; +import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED; import android.text.TextUtils; -import android.util.JsonReader; -import android.util.JsonWriter; -import android.util.Log; -import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; +import com.android.inputmethod.annotations.UsedForTesting; +import com.android.inputmethod.latin.Constants; + import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.Arrays; import java.util.Locale; public final class StringUtils { @@ -39,6 +33,8 @@ public final class StringUtils { public static final int CAPITALIZE_FIRST = 1; // First only public static final int CAPITALIZE_ALL = 2; // All caps + private static final String EMPTY_STRING = ""; + private StringUtils() { // This utility class is not publicly instantiable. } @@ -50,7 +46,7 @@ public final class StringUtils { public static String newSingleCodePointString(int codePoint) { if (Character.charCount(codePoint) == 1) { - // Optimization: avoid creating an temporary array for characters that are + // Optimization: avoid creating a temporary array for characters that are // represented by a single char value return String.valueOf((char) codePoint); } @@ -80,6 +76,20 @@ public final class StringUtils { return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT)); } + public static String joinCommaSplittableText(final String head, final String tail) { + if (TextUtils.isEmpty(head) && TextUtils.isEmpty(tail)) { + return EMPTY_STRING; + } + // Here either head or tail is not null. + if (TextUtils.isEmpty(head)) { + return tail; + } + if (TextUtils.isEmpty(tail)) { + return head; + } + return head + SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT + tail; + } + public static String appendToCommaSplittableTextIfNotExists(final String text, final String extraValues) { if (TextUtils.isEmpty(extraValues)) { @@ -94,7 +104,7 @@ public final class StringUtils { public static String removeFromCommaSplittableTextIfExists(final String text, final String extraValues) { if (TextUtils.isEmpty(extraValues)) { - return ""; + return EMPTY_STRING; } final String[] elements = extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT); if (!containsInArray(text, elements)) { @@ -162,20 +172,87 @@ public final class StringUtils { private static final int[] EMPTY_CODEPOINTS = {}; - public static int[] toCodePointArray(final String string) { - final int length = string.length(); + public static int[] toCodePointArray(final CharSequence charSequence) { + return toCodePointArray(charSequence, 0, charSequence.length()); + } + + /** + * Converts a range of a string to an array of code points. + * @param charSequence the source string. + * @param startIndex the start index inside the string in java chars, inclusive. + * @param endIndex the end index inside the string in java chars, exclusive. + * @return a new array of code points. At most endIndex - startIndex, but possibly less. + */ + public static int[] toCodePointArray(final CharSequence charSequence, + final int startIndex, final int endIndex) { + final int length = charSequence.length(); if (length <= 0) { return EMPTY_CODEPOINTS; } - final int[] codePoints = new int[string.codePointCount(0, length)]; + final int[] codePoints = + new int[Character.codePointCount(charSequence, startIndex, endIndex)]; + copyCodePointsAndReturnCodePointCount(codePoints, charSequence, startIndex, endIndex, + false /* downCase */); + return codePoints; + } + + /** + * Copies the codepoints in a CharSequence to an int array. + * + * This method assumes there is enough space in the array to store the code points. The size + * can be measured with Character#codePointCount(CharSequence, int, int) before passing to this + * method. If the int array is too small, an ArrayIndexOutOfBoundsException will be thrown. + * Also, this method makes no effort to be thread-safe. Do not modify the CharSequence while + * this method is running, or the behavior is undefined. + * This method can optionally downcase code points before copying them, but it pays no attention + * to locale while doing so. + * + * @param destination the int array. + * @param charSequence the CharSequence. + * @param startIndex the start index inside the string in java chars, inclusive. + * @param endIndex the end index inside the string in java chars, exclusive. + * @param downCase if this is true, code points will be downcased before being copied. + * @return the number of copied code points. + */ + public static int copyCodePointsAndReturnCodePointCount(final int[] destination, + final CharSequence charSequence, final int startIndex, final int endIndex, + final boolean downCase) { int destIndex = 0; - for (int index = 0; index < length; index = string.offsetByCodePoints(index, 1)) { - codePoints[destIndex] = string.codePointAt(index); + for (int index = startIndex; index < endIndex; + index = Character.offsetByCodePoints(charSequence, index, 1)) { + final int codePoint = Character.codePointAt(charSequence, index); + // TODO: stop using this, as it's not aware of the locale and does not always do + // the right thing. + destination[destIndex] = downCase ? Character.toLowerCase(codePoint) : codePoint; destIndex++; } + return destIndex; + } + + public static int[] toSortedCodePointArray(final String string) { + final int[] codePoints = toCodePointArray(string); + Arrays.sort(codePoints); return codePoints; } + /** + * Construct a String from a code point array + * + * @param codePoints a code point array that is null terminated when its logical length is + * shorter than the array length. + * @return a string constructed from the code point array. + */ + public static String getStringFromNullTerminatedCodePointArray(final int[] codePoints) { + int stringLength = codePoints.length; + for (int i = 0; i < codePoints.length; i++) { + if (codePoints[i] == 0) { + stringLength = i; + break; + } + } + return new String(codePoints, 0 /* offset */, stringLength); + } + // This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE. public static int getCapitalizationType(final String text) { // If the first char is not uppercase, then the word is either all lower case or @@ -239,65 +316,58 @@ public final class StringUtils { return true; } - @UsedForTesting - public static boolean looksValidForDictionaryInsertion(final CharSequence text, - final SettingsValues settings) { - if (TextUtils.isEmpty(text)) return false; + /** + * Returns true if all code points in text are whitespace, false otherwise. Empty is true. + */ + // Interestingly enough, U+00A0 NO-BREAK SPACE and U+200B ZERO-WIDTH SPACE are not considered + // whitespace, while EN SPACE, EM SPACE and IDEOGRAPHIC SPACES are. + public static boolean containsOnlyWhitespace(final String text) { final int length = text.length(); int i = 0; - int digitCount = 0; while (i < length) { - final int codePoint = Character.codePointAt(text, i); - final int charCount = Character.charCount(codePoint); - i += charCount; - if (Character.isDigit(codePoint)) { - // Count digits: see below - digitCount += charCount; - continue; + final int codePoint = text.codePointAt(i); + if (!Character.isWhitespace(codePoint)) { + return false; } - if (!settings.isWordCodePoint(codePoint)) return false; + i += Character.charCount(codePoint); } - // We reject strings entirely comprised of digits to avoid using PIN codes or credit - // card numbers. It would come in handy for word prediction though; a good example is - // when writing one's address where the street number is usually quite discriminative, - // as well as the postal code. - return digitCount < length; + return true; } public static boolean isIdenticalAfterCapitalizeEachWord(final String text, - final String separators) { - boolean needCapsNext = true; + final int[] sortedSeparators) { + boolean needsCapsNext = true; final int len = text.length(); for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { final int codePoint = text.codePointAt(i); if (Character.isLetter(codePoint)) { - if ((needCapsNext && !Character.isUpperCase(codePoint)) - || (!needCapsNext && !Character.isLowerCase(codePoint))) { + if ((needsCapsNext && !Character.isUpperCase(codePoint)) + || (!needsCapsNext && !Character.isLowerCase(codePoint))) { return false; } } // We need a capital letter next if this is a separator. - needCapsNext = (-1 != separators.indexOf(codePoint)); + needsCapsNext = (Arrays.binarySearch(sortedSeparators, codePoint) >= 0); } return true; } // TODO: like capitalizeFirst*, this does not work perfectly for Dutch because of the IJ digraph // which should be capitalized together in *some* cases. - public static String capitalizeEachWord(final String text, final String separators, + public static String capitalizeEachWord(final String text, final int[] sortedSeparators, final Locale locale) { final StringBuilder builder = new StringBuilder(); - boolean needCapsNext = true; + boolean needsCapsNext = true; final int len = text.length(); for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) { final String nextChar = text.substring(i, text.offsetByCodePoints(i, 1)); - if (needCapsNext) { + if (needsCapsNext) { builder.append(nextChar.toUpperCase(locale)); } else { builder.append(nextChar.toLowerCase(locale)); } // We need a capital letter next if this is a separator. - needCapsNext = (-1 != separators.indexOf(nextChar.codePointAt(0))); + needsCapsNext = (Arrays.binarySearch(sortedSeparators, nextChar.codePointAt(0)) >= 0); } return builder.toString(); } @@ -328,7 +398,7 @@ public final class StringUtils { boolean hasPeriod = false; int codePoint = 0; while (i > 0) { - codePoint = Character.codePointBefore(text, i); + codePoint = Character.codePointBefore(text, i); if (codePoint < Constants.CODE_PERIOD || codePoint > 'z') { // Handwavy heuristic to see if that's a URL character. Anything between period // and z. This includes all lower- and upper-case ascii letters, period, @@ -367,7 +437,49 @@ public final class StringUtils { return false; } - public static boolean isEmptyStringOrWhiteSpaces(String s) { + /** + * Examines the string and returns whether we're inside a double quote. + * + * This is used to decide whether we should put an automatic space before or after a double + * quote character. If we're inside a quotation, then we want to close it, so we want a space + * after and not before. Otherwise, we want to open the quotation, so we want a space before + * and not after. Exception: after a digit, we never want a space because the "inch" or + * "minutes" use cases is dominant after digits. + * In the practice, we determine whether we are in a quotation or not by finding the previous + * double quote character, and looking at whether it's followed by whitespace. If so, that + * was a closing quotation mark, so we're not inside a double quote. If it's not followed + * by whitespace, then it was an opening quotation mark, and we're inside a quotation. + * + * @param text the text to examine. + * @return whether we're inside a double quote. + */ + public static boolean isInsideDoubleQuoteOrAfterDigit(final CharSequence text) { + int i = text.length(); + if (0 == i) return false; + int codePoint = Character.codePointBefore(text, i); + if (Character.isDigit(codePoint)) return true; + int prevCodePoint = 0; + while (i > 0) { + codePoint = Character.codePointBefore(text, i); + if (Constants.CODE_DOUBLE_QUOTE == codePoint) { + // If we see a double quote followed by whitespace, then that + // was a closing quote. + if (Character.isWhitespace(prevCodePoint)) return false; + } + if (Character.isWhitespace(codePoint) && Constants.CODE_DOUBLE_QUOTE == prevCodePoint) { + // If we see a double quote preceded by whitespace, then that + // was an opening quote. No need to continue seeking. + return true; + } + i -= Character.charCount(codePoint); + prevCodePoint = codePoint; + } + // We reached the start of text. If the first char is a double quote, then we're inside + // a double quote. Otherwise we're not. + return Constants.CODE_DOUBLE_QUOTE == codePoint; + } + + public static boolean isEmptyStringOrWhiteSpaces(final String s) { final int N = codePointCount(s); for (int i = 0; i < N; ++i) { if (!Character.isWhitespace(s.codePointAt(i))) { @@ -378,9 +490,9 @@ public final class StringUtils { } @UsedForTesting - public static String byteArrayToHexString(byte[] bytes) { + public static String byteArrayToHexString(final byte[] bytes) { if (bytes == null || bytes.length == 0) { - return ""; + return EMPTY_STRING; } final StringBuilder sb = new StringBuilder(); for (byte b : bytes) { @@ -393,7 +505,7 @@ public final class StringUtils { * Convert hex string to byte array. The string length must be an even number. */ @UsedForTesting - public static byte[] hexStringToByteArray(String hexString) { + public static byte[] hexStringToByteArray(final String hexString) { if (TextUtils.isEmpty(hexString)) { return null; } @@ -410,66 +522,59 @@ public final class StringUtils { return bytes; } - public static List<Object> jsonStrToList(String s) { - final ArrayList<Object> retval = CollectionUtils.newArrayList(); - final JsonReader reader = new JsonReader(new StringReader(s)); - try { - reader.beginArray(); - while(reader.hasNext()) { - reader.beginObject(); - while (reader.hasNext()) { - final String name = reader.nextName(); - if (name.equals(Integer.class.getSimpleName())) { - retval.add(reader.nextInt()); - } else if (name.equals(String.class.getSimpleName())) { - retval.add(reader.nextString()); - } else { - Log.w(TAG, "Invalid name: " + name); - reader.skipValue(); - } - } - reader.endObject(); - } - reader.endArray(); - return retval; - } catch (IOException e) { - } finally { - try { - reader.close(); - } catch (IOException e) { + public static String toUpperCaseOfStringForLocale(final String text, + final boolean needsToUpperCase, final Locale locale) { + if (text == null || !needsToUpperCase) return text; + return text.toUpperCase(locale); + } + + public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase, + final Locale locale) { + if (!Constants.isLetterCode(code) || !needsToUpperCase) return code; + final String text = newSingleCodePointString(code); + final String casedText = toUpperCaseOfStringForLocale( + text, needsToUpperCase, locale); + return codePointCount(casedText) == 1 + ? casedText.codePointAt(0) : CODE_UNSPECIFIED; + } + + @UsedForTesting + public static class Stringizer<E> { + public String stringize(final E element) { + return element != null ? element.toString() : "null"; + } + + @UsedForTesting + public final String join(final E[] array) { + return joinStringArray(toStringArray(array), null /* delimiter */); + } + + @UsedForTesting + public final String join(final E[] array, final String delimiter) { + return joinStringArray(toStringArray(array), delimiter); + } + + protected String[] toStringArray(final E[] array) { + final String[] stringArray = new String[array.length]; + for (int index = 0; index < array.length; index++) { + stringArray[index] = stringize(array[index]); } + return stringArray; } - return Collections.<Object>emptyList(); - } - public static String listToJsonStr(List<Object> list) { - if (list == null || list.isEmpty()) { - return ""; - } - final StringWriter sw = new StringWriter(); - final JsonWriter writer = new JsonWriter(sw); - try { - writer.beginArray(); - for (final Object o : list) { - writer.beginObject(); - if (o instanceof Integer) { - writer.name(Integer.class.getSimpleName()).value((Integer)o); - } else if (o instanceof String) { - writer.name(String.class.getSimpleName()).value((String)o); - } - writer.endObject(); + protected String joinStringArray(final String[] stringArray, final String delimiter) { + if (stringArray == null) { + return "null"; } - writer.endArray(); - return sw.toString(); - } catch (IOException e) { - } finally { - try { - if (writer != null) { - writer.close(); - } - } catch (IOException e) { + if (delimiter == null) { + return Arrays.toString(stringArray); + } + final StringBuilder sb = new StringBuilder(); + for (int index = 0; index < stringArray.length; index++) { + sb.append(index == 0 ? "[" : delimiter); + sb.append(stringArray[index]); } + return sb + "]"; } - return ""; } } diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java index 102a41b4e..b37779bdc 100644 --- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java @@ -25,17 +25,18 @@ import android.os.Build; import android.util.Log; import android.view.inputmethod.InputMethodSubtype; -import com.android.inputmethod.latin.DictionaryFactory; +import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; +import java.util.Arrays; import java.util.HashMap; import java.util.Locale; public final class SubtypeLocaleUtils { - static final String TAG = SubtypeLocaleUtils.class.getSimpleName(); - // This class must be located in the same package as LatinIME.java. - private static final String RESOURCE_PACKAGE_NAME = - DictionaryFactory.class.getPackage().getName(); + private static final String TAG = SubtypeLocaleUtils.class.getSimpleName(); + + // This reference class {@link Constants} must be located in the same package as LatinIME.java. + private static final String RESOURCE_PACKAGE_NAME = Constants.class.getPackage().getName(); // Special language code to represent "no language". public static final String NO_LANGUAGE = "zz"; @@ -43,7 +44,8 @@ public final class SubtypeLocaleUtils { public static final String EMOJI = "emoji"; public static final int UNKNOWN_KEYBOARD_LAYOUT = R.string.subtype_generic; - private static boolean sInitialized = false; + private static volatile boolean sInitialized = false; + private static final Object sInitializeLock = new Object(); private static Resources sResources; private static String[] sPredefinedKeyboardLayoutSet; // Keyboard layout to its display name map. @@ -76,9 +78,16 @@ public final class SubtypeLocaleUtils { } // Note that this initialization method can be called multiple times. - public static synchronized void init(final Context context) { - if (sInitialized) return; + public static void init(final Context context) { + synchronized (sInitializeLock) { + if (sInitialized == false) { + initLocked(context); + sInitialized = true; + } + } + } + private static void initLocked(final Context context) { final Resources res = context.getResources(); sResources = res; @@ -121,8 +130,6 @@ public final class SubtypeLocaleUtils { final String keyboardLayoutSet = keyboardLayoutSetMap[i + 1]; sLocaleAndExtraValueToKeyboardLayoutSetMap.put(key, keyboardLayoutSet); } - - sInitialized = true; } public static String[] getPredefinedKeyboardLayoutSet() { @@ -166,8 +173,18 @@ public final class SubtypeLocaleUtils { return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale); } + public static String getSubtypeLanguageDisplayName(final String localeString) { + final Locale locale = LocaleUtils.constructLocaleFromString(localeString); + final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString); + return getSubtypeLocaleDisplayNameInternal(locale.getLanguage(), displayLocale); + } + private static String getSubtypeLocaleDisplayNameInternal(final String localeString, final Locale displayLocale) { + if (NO_LANGUAGE.equals(localeString)) { + // No language subtype should be displayed in system locale. + return sResources.getString(R.string.subtype_no_language); + } final Integer exceptionalNameResId = sExceptionalLocaleToNameIdsMap.get(localeString); final String displayName; if (exceptionalNameResId != null) { @@ -178,9 +195,6 @@ public final class SubtypeLocaleUtils { } }; displayName = getExceptionalName.runInLocale(sResources, displayLocale); - } else if (NO_LANGUAGE.equals(localeString)) { - // No language subtype should be displayed in system locale. - return sResources.getString(R.string.subtype_no_language); } else { final Locale locale = LocaleUtils.constructLocaleFromString(localeString); displayName = locale.getDisplayName(displayLocale); @@ -197,12 +211,14 @@ public final class SubtypeLocaleUtils { // es_US spanish F Español (EE.UU.) exception // fr azerty F Français // fr_CA qwerty F Français (Canada) + // fr_CH swiss F Français (Suisse) // de qwertz F Deutsch - // zz qwerty F No language (QWERTY) in system locale + // de_CH swiss T Deutsch (Schweiz) + // zz qwerty F Alphabet (QWERTY) in system locale // fr qwertz T Français (QWERTZ) // de qwerty T Deutsch (QWERTY) // en_US azerty T English (US) (AZERTY) exception - // zz azerty T No language (AZERTY) in system locale + // zz azerty T Alphabet (AZERTY) in system locale private static String getReplacementString(final InputMethodSubtype subtype, final Locale displayLocale) { @@ -289,45 +305,23 @@ public final class SubtypeLocaleUtils { return keyboardLayoutSet; } - // InputMethodSubtype's display name for spacebar text in its locale. - // isAdditionalSubtype (T=true, F=false) - // locale layout | Short Middle Full - // ------ ------- - ---- --------- ---------------------- - // en_US qwerty F En English English (US) exception - // en_GB qwerty F En English English (UK) exception - // es_US spanish F Es Español Español (EE.UU.) exception - // fr azerty F Fr Français Français - // fr_CA qwerty F Fr Français Français (Canada) - // de qwertz F De Deutsch Deutsch - // zz qwerty F QWERTY QWERTY - // fr qwertz T Fr Français Français - // de qwerty T De Deutsch Deutsch - // en_US azerty T En 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 (isNoLanguage(subtype)) { - return getKeyboardLayoutSetDisplayName(subtype); - } - return getSubtypeLocaleDisplayName(subtype.getLocale()); + // TODO: Get this information from the framework instead of maintaining here by ourselves. + // Sorted list of known Right-To-Left language codes. + private static final String[] SORTED_RTL_LANGUAGES = { + "ar", // Arabic + "fa", // Persian + "iw", // Hebrew + }; + static { + Arrays.sort(SORTED_RTL_LANGUAGES); } - // Get InputMethodSubtype's middle display name in its locale. - public static String getMiddleDisplayName(final InputMethodSubtype subtype) { - if (isNoLanguage(subtype)) { - return getKeyboardLayoutSetDisplayName(subtype); - } - final Locale locale = getSubtypeLocale(subtype); - return getSubtypeLocaleDisplayName(locale.getLanguage()); + public static boolean isRtlLanguage(final Locale locale) { + final String language = locale.getLanguage(); + return Arrays.binarySearch(SORTED_RTL_LANGUAGES, language) >= 0; } - // Get InputMethodSubtype's short display name in its locale. - public static String getShortDisplayName(final InputMethodSubtype subtype) { - if (isNoLanguage(subtype)) { - return ""; - } - final Locale locale = getSubtypeLocale(subtype); - return StringUtils.capitalizeFirstCodePoint(locale.getLanguage(), locale); + public static boolean isRtlLanguage(final InputMethodSubtype subtype) { + return isRtlLanguage(getSubtypeLocale(subtype)); } } diff --git a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java new file mode 100644 index 000000000..0b362c48a --- /dev/null +++ b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java @@ -0,0 +1,76 @@ +/* + * 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 com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Locale; +import java.util.TreeSet; + +/** + * A TreeSet of SuggestedWordInfo that is bounded in size and throws everything that's smaller + * than its limit + */ +public final class SuggestionResults extends TreeSet<SuggestedWordInfo> { + public final Locale mLocale; + private final int mCapacity; + + public SuggestionResults(final Locale locale, final int capacity) { + this(locale, sSuggestedWordInfoComparator, capacity); + } + + public SuggestionResults(final Locale locale, final Comparator<SuggestedWordInfo> comparator, + final int capacity) { + super(comparator); + mLocale = locale; + mCapacity = capacity; + } + + @Override + public boolean add(final SuggestedWordInfo e) { + if (size() < mCapacity) return super.add(e); + if (comparator().compare(e, last()) > 0) return false; + super.add(e); + pollLast(); // removes the last element + return true; + } + + @Override + public boolean addAll(final Collection<? extends SuggestedWordInfo> e) { + if (null == e) return false; + return super.addAll(e); + } + + private static final class SuggestedWordInfoComparator + implements Comparator<SuggestedWordInfo> { + // This comparator ranks the word info with the higher frequency first. That's because + // that's the order we want our elements in. + @Override + public int compare(final SuggestedWordInfo o1, final SuggestedWordInfo o2) { + if (o1.mScore > o2.mScore) return -1; + if (o1.mScore < o2.mScore) return 1; + if (o1.mCodePointCount < o2.mCodePointCount) return -1; + if (o1.mCodePointCount > o2.mCodePointCount) return 1; + return o1.mWord.compareTo(o2.mWord); + } + } + + private static final SuggestedWordInfoComparator sSuggestedWordInfoComparator = + new SuggestedWordInfoComparator(); +} diff --git a/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java b/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java index afbe2ecad..42ea3c959 100644 --- a/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java +++ b/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java @@ -22,6 +22,8 @@ import android.content.pm.PackageManager; import android.os.AsyncTask; import android.util.LruCache; +import com.android.inputmethod.compat.AppWorkaroundsUtils; + public final class TargetPackageInfoGetterTask extends AsyncTask<String, Void, PackageInfo> { private static final int MAX_CACHE_ENTRIES = 64; // arbitrary @@ -37,17 +39,13 @@ public final class TargetPackageInfoGetterTask extends sCache.remove(packageName); } - public interface OnTargetPackageInfoKnownListener { - public void onTargetPackageInfoKnown(final PackageInfo info); - } - private Context mContext; - private final OnTargetPackageInfoKnownListener mListener; + private final AsyncResultHolder<AppWorkaroundsUtils> mResult; public TargetPackageInfoGetterTask(final Context context, - final OnTargetPackageInfoKnownListener listener) { + final AsyncResultHolder<AppWorkaroundsUtils> result) { mContext = context; - mListener = listener; + mResult = result; } @Override @@ -65,6 +63,6 @@ public final class TargetPackageInfoGetterTask extends @Override protected void onPostExecute(final PackageInfo info) { - mListener.onTargetPackageInfoKnown(info); + mResult.set(new AppWorkaroundsUtils(info)); } } diff --git a/java/src/com/android/inputmethod/latin/utils/TextRange.java b/java/src/com/android/inputmethod/latin/utils/TextRange.java index 48b443ddd..dbf3b5060 100644 --- a/java/src/com/android/inputmethod/latin/utils/TextRange.java +++ b/java/src/com/android/inputmethod/latin/utils/TextRange.java @@ -31,6 +31,7 @@ public final class TextRange { private final int mCursorIndex; public final CharSequence mWord; + public final boolean mHasUrlSpans; public int getNumberOfCharsInWordBeforeCursor() { return mCursorIndex - mWordAtCursorStartIndex; @@ -95,7 +96,7 @@ public final class TextRange { } } if (spanStart == mWordAtCursorStartIndex && spanEnd == mWordAtCursorEndIndex) { - // If the span does not start and stop here, we ignore it. It probably extends + // If the span does not start and stop here, ignore it. It probably extends // past the start or end of the word, as happens in missing space correction // or EasyEditSpans put by voice input. spans[writeIndex++] = spans[readIndex]; @@ -105,7 +106,7 @@ public final class TextRange { } public TextRange(final CharSequence textAtCursor, final int wordAtCursorStartIndex, - final int wordAtCursorEndIndex, final int cursorIndex) { + final int wordAtCursorEndIndex, final int cursorIndex, final boolean hasUrlSpans) { if (wordAtCursorStartIndex < 0 || cursorIndex < wordAtCursorStartIndex || cursorIndex > wordAtCursorEndIndex || wordAtCursorEndIndex > textAtCursor.length()) { @@ -115,6 +116,7 @@ public final class TextRange { mWordAtCursorStartIndex = wordAtCursorStartIndex; mWordAtCursorEndIndex = wordAtCursorEndIndex; mCursorIndex = cursorIndex; + mHasUrlSpans = hasUrlSpans; mWord = mTextAtCursor.subSequence(mWordAtCursorStartIndex, mWordAtCursorEndIndex); } }
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java b/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java index 47ea1ea75..087a7f255 100644 --- a/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java +++ b/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java @@ -22,6 +22,9 @@ import android.graphics.Typeface; import android.util.SparseArray; public final class TypefaceUtils { + private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' }; + private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' }; + private TypefaceUtils() { // This utility class is not publicly instantiable. } @@ -31,7 +34,7 @@ public final class TypefaceUtils { // Working variable for the following method. private static final Rect sTextHeightBounds = new Rect(); - public static float getCharHeight(final char[] referenceChar, final Paint paint) { + private static float getCharHeight(final char[] referenceChar, final Paint paint) { final int key = getCharGeometryCacheKey(referenceChar[0], paint); synchronized (sTextHeightCache) { final Float cachedValue = sTextHeightCache.get(key); @@ -51,7 +54,7 @@ public final class TypefaceUtils { // Working variable for the following method. private static final Rect sTextWidthBounds = new Rect(); - public static float getCharWidth(final char[] referenceChar, final Paint paint) { + private static float getCharWidth(final char[] referenceChar, final Paint paint) { final int key = getCharGeometryCacheKey(referenceChar[0], paint); synchronized (sTextWidthCache) { final Float cachedValue = sTextWidthCache.get(key); @@ -66,11 +69,6 @@ public final class TypefaceUtils { } } - public static float getStringWidth(final String string, final Paint paint) { - paint.getTextBounds(string, 0, string.length(), sTextWidthBounds); - return sTextWidthBounds.width(); - } - private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) { final int labelSize = (int)paint.getTextSize(); final Typeface face = paint.getTypeface(); @@ -86,9 +84,25 @@ public final class TypefaceUtils { } } - public static float getLabelWidth(final String label, final Paint paint) { - final Rect textBounds = new Rect(); - paint.getTextBounds(label, 0, label.length(), textBounds); - return textBounds.width(); + public static float getReferenceCharHeight(final Paint paint) { + return getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint); + } + + public static float getReferenceCharWidth(final Paint paint) { + return getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint); + } + + public static float getReferenceDigitWidth(final Paint paint) { + return getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint); + } + + // Working variable for the following method. + private static final Rect sStringWidthBounds = new Rect(); + + public static float getStringWidth(final String string, final Paint paint) { + synchronized (sStringWidthBounds) { + paint.getTextBounds(string, 0, string.length(), sStringWidthBounds); + return sStringWidthBounds.width(); + } } } diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java deleted file mode 100644 index 635afe7cc..000000000 --- a/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2012 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.util.Log; - -import com.android.inputmethod.annotations.UsedForTesting; -import com.android.inputmethod.latin.makedict.BinaryDictIOUtils; -import com.android.inputmethod.latin.makedict.DictDecoder; -import com.android.inputmethod.latin.makedict.DictEncoder; -import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions; -import com.android.inputmethod.latin.makedict.FusionDictionary; -import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray; -import com.android.inputmethod.latin.makedict.PendingAttribute; -import com.android.inputmethod.latin.makedict.UnsupportedFormatException; -import com.android.inputmethod.latin.personalization.UserHistoryDictionaryBigramList; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.concurrent.TimeUnit; - -/** - * Reads and writes Binary files for a UserHistoryDictionary. - * - * All the methods in this class are static. - */ -public final class UserHistoryDictIOUtils { - private static final String TAG = UserHistoryDictIOUtils.class.getSimpleName(); - private static final boolean DEBUG = false; - private static final String USES_FORGETTING_CURVE_KEY = "USES_FORGETTING_CURVE"; - private static final String USES_FORGETTING_CURVE_VALUE = "1"; - private static final String LAST_UPDATED_TIME_KEY = "date"; - - public interface OnAddWordListener { - /** - * Callback to be notified when a word is added to the dictionary. - * @param word The added word. - * @param shortcutTarget A shortcut target for this word, or null if none. - * @param frequency The frequency for this word. - * @param shortcutFreq The frequency of the shortcut (0~15, with 15 = whitelist). - * Unspecified if shortcutTarget is null - do not rely on its value. - */ - public void setUnigram(final String word, final String shortcutTarget, final int frequency, - final int shortcutFreq); - public void setBigram(final String word1, final String word2, final int frequency); - } - - @UsedForTesting - public interface BigramDictionaryInterface { - public int getFrequency(final String word1, final String word2); - } - - /** - * Writes dictionary to file. - */ - public static void writeDictionary(final DictEncoder dictEncoder, - final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams, - final FormatOptions formatOptions) { - final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams); - fusionDict.addOptionAttribute(USES_FORGETTING_CURVE_KEY, USES_FORGETTING_CURVE_VALUE); - fusionDict.addOptionAttribute(LAST_UPDATED_TIME_KEY, - String.valueOf(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()))); - try { - dictEncoder.writeDictionary(fusionDict, formatOptions); - Log.d(TAG, "end writing"); - } catch (IOException e) { - Log.e(TAG, "IO exception while writing file", e); - } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported format", e); - } - } - - /** - * Constructs a new FusionDictionary from BigramDictionaryInterface. - */ - @UsedForTesting - static FusionDictionary constructFusionDictionary( - final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams) { - final FusionDictionary fusionDict = new FusionDictionary(new PtNodeArray(), - new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false, - false)); - int profTotal = 0; - for (final String word1 : bigrams.keySet()) { - final HashMap<String, Byte> word1Bigrams = bigrams.getBigrams(word1); - for (final String word2 : word1Bigrams.keySet()) { - final int freq = dict.getFrequency(word1, word2); - if (freq == -1) { - // don't add this bigram. - continue; - } - if (DEBUG) { - if (word1 == null) { - Log.d(TAG, "add unigram: " + word2 + "," + Integer.toString(freq)); - } else { - Log.d(TAG, "add bigram: " + word1 - + "," + word2 + "," + Integer.toString(freq)); - } - profTotal++; - } - if (word1 == null) { // unigram - fusionDict.add(word2, freq, null, false /* isNotAWord */); - } else { // bigram - if (FusionDictionary.findWordInTree(fusionDict.mRootNodeArray, word1) == null) { - fusionDict.add(word1, 2, null, false /* isNotAWord */); - } - fusionDict.setBigram(word1, word2, freq); - } - bigrams.updateBigram(word1, word2, (byte)freq); - } - } - if (DEBUG) { - Log.d(TAG, "add " + profTotal + "words"); - } - return fusionDict; - } - - /** - * Reads dictionary from file. - */ - public static void readDictionaryBinary(final DictDecoder dictDecoder, - final OnAddWordListener dict) { - final TreeMap<Integer, String> unigrams = CollectionUtils.newTreeMap(); - final TreeMap<Integer, Integer> frequencies = CollectionUtils.newTreeMap(); - final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap(); - try { - dictDecoder.readUnigramsAndBigramsBinary(unigrams, frequencies, bigrams); - } catch (IOException e) { - Log.e(TAG, "IO exception while reading file", e); - } catch (UnsupportedFormatException e) { - Log.e(TAG, "Unsupported format", e); - } catch (ArrayIndexOutOfBoundsException e) { - Log.e(TAG, "ArrayIndexOutOfBoundsException while reading file", e); - } - addWordsFromWordMap(unigrams, frequencies, bigrams, dict); - } - - /** - * Adds all unigrams and bigrams in maps to OnAddWordListener. - */ - @UsedForTesting - static void addWordsFromWordMap(final TreeMap<Integer, String> unigrams, - final TreeMap<Integer, Integer> frequencies, - final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams, - final OnAddWordListener to) { - for (Entry<Integer, String> entry : unigrams.entrySet()) { - final String word1 = entry.getValue(); - final int unigramFrequency = frequencies.get(entry.getKey()); - to.setUnigram(word1, null /* shortcutTarget */, unigramFrequency, 0 /* shortcutFreq */); - final ArrayList<PendingAttribute> attrList = bigrams.get(entry.getKey()); - if (attrList != null) { - for (final PendingAttribute attr : attrList) { - final String word2 = unigrams.get(attr.mAddress); - if (word1 == null || word2 == null) { - Log.e(TAG, "Invalid bigram pair detected: " + word1 + ", " + word2); - continue; - } - to.setBigram(word1, word2, - BinaryDictIOUtils.reconstructBigramFrequency(unigramFrequency, - attr.mFrequency)); - } - } - } - - } -} diff --git a/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java deleted file mode 100644 index 1992b2f5d..000000000 --- a/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (C) 2012 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.util.Log; - -import java.util.concurrent.TimeUnit; - -public final class UserHistoryForgettingCurveUtils { - private static final String TAG = UserHistoryForgettingCurveUtils.class.getSimpleName(); - private static final boolean DEBUG = false; - private static final int DEFAULT_FC_FREQ = 127; - private static final int BOOSTED_FC_FREQ = 200; - private static int FC_FREQ_MAX = DEFAULT_FC_FREQ; - /* package */ static final int COUNT_MAX = 3; - private static final int FC_LEVEL_MAX = 3; - /* package */ static final int ELAPSED_TIME_MAX = 15; - private static final int ELAPSED_TIME_INTERVAL_HOURS = 6; - private static final long ELAPSED_TIME_INTERVAL_MILLIS = - TimeUnit.HOURS.toMillis(ELAPSED_TIME_INTERVAL_HOURS); - private static final int HALF_LIFE_HOURS = 48; - private static final int MAX_PUSH_ELAPSED = (FC_LEVEL_MAX + 1) * (ELAPSED_TIME_MAX + 1); - - public static void boostMaxFreqForDebug() { - FC_FREQ_MAX = BOOSTED_FC_FREQ; - } - - public static void resetMaxFreqForDebug() { - FC_FREQ_MAX = DEFAULT_FC_FREQ; - } - - private UserHistoryForgettingCurveUtils() { - // This utility class is not publicly instantiable. - } - - public static final class ForgettingCurveParams { - private byte mFc; - long mLastTouchedTime = 0; - private final boolean mIsValid; - - private void updateLastTouchedTime() { - mLastTouchedTime = System.currentTimeMillis(); - } - - public ForgettingCurveParams(boolean isValid) { - this(System.currentTimeMillis(), isValid); - } - - private ForgettingCurveParams(long now, boolean isValid) { - this(pushCount((byte)0, isValid), now, now, isValid); - } - - /** This constructor is called when the user history bigram dictionary is being restored. */ - public ForgettingCurveParams(int fc, long now, long last) { - // All words with level >= 1 had been saved. - // Invalid words with level == 0 had been saved. - // Valid words words with level == 0 had *not* been saved. - this(fc, now, last, fcToLevel((byte)fc) > 0); - } - - private ForgettingCurveParams(int fc, long now, long last, boolean isValid) { - mIsValid = isValid; - mFc = (byte)fc; - mLastTouchedTime = last; - updateElapsedTime(now); - } - - public boolean isValid() { - return mIsValid; - } - - public byte getFc() { - updateElapsedTime(System.currentTimeMillis()); - return mFc; - } - - public int getFrequency() { - updateElapsedTime(System.currentTimeMillis()); - return UserHistoryForgettingCurveUtils.fcToFreq(mFc); - } - - public int notifyTypedAgainAndGetFrequency() { - updateLastTouchedTime(); - // TODO: Check whether this word is valid or not - mFc = pushCount(mFc, false); - return UserHistoryForgettingCurveUtils.fcToFreq(mFc); - } - - private void updateElapsedTime(long now) { - final int elapsedTimeCount = - (int)((now - mLastTouchedTime) / ELAPSED_TIME_INTERVAL_MILLIS); - if (elapsedTimeCount <= 0) { - return; - } - if (elapsedTimeCount >= MAX_PUSH_ELAPSED) { - mLastTouchedTime = now; - mFc = 0; - return; - } - for (int i = 0; i < elapsedTimeCount; ++i) { - mLastTouchedTime += ELAPSED_TIME_INTERVAL_MILLIS; - mFc = pushElapsedTime(mFc); - } - } - } - - /* package */ static int fcToElapsedTime(byte fc) { - return fc & 0x0F; - } - - /* package */ static int fcToCount(byte fc) { - return (fc >> 4) & 0x03; - } - - /* package */ static int fcToLevel(byte fc) { - return (fc >> 6) & 0x03; - } - - private static int calcFreq(int elapsedTime, int count, int level) { - if (level <= 0) { - // Reserved words, just return -1 - return -1; - } - if (count == COUNT_MAX) { - // Temporary promote because it's frequently typed recently - ++level; - } - final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime)); - final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level)); - return MathUtils.SCORE_TABLE[l - 1][et]; - } - - /* pakcage */ static byte calcFc(int elapsedTime, int count, int level) { - final int et = Math.min(FC_FREQ_MAX, Math.max(0, elapsedTime)); - final int c = Math.min(COUNT_MAX, Math.max(0, count)); - final int l = Math.min(FC_LEVEL_MAX, Math.max(0, level)); - return (byte)(et | (c << 4) | (l << 6)); - } - - public static int fcToFreq(byte fc) { - final int elapsedTime = fcToElapsedTime(fc); - final int count = fcToCount(fc); - final int level = fcToLevel(fc); - return calcFreq(elapsedTime, count, level); - } - - public static byte pushElapsedTime(byte fc) { - int elapsedTime = fcToElapsedTime(fc); - int count = fcToCount(fc); - int level = fcToLevel(fc); - if (elapsedTime >= ELAPSED_TIME_MAX) { - // Downgrade level - elapsedTime = 0; - count = COUNT_MAX; - --level; - } else { - ++elapsedTime; - } - return calcFc(elapsedTime, count, level); - } - - public static byte pushCount(byte fc, boolean isValid) { - final int elapsedTime = fcToElapsedTime(fc); - int count = fcToCount(fc); - int level = fcToLevel(fc); - if ((elapsedTime == 0 && count >= COUNT_MAX) || (isValid && level == 0)) { - // Upgrade level - ++level; - count = 0; - if (DEBUG) { - Log.d(TAG, "Upgrade level."); - } - } else { - ++count; - } - return calcFc(0, count, level); - } - - // TODO: isValid should be false for a word whose frequency is 0, - // or that is not in the dictionary. - /** - * Check wheather we should save the bigram to the SQL DB or not - */ - public static boolean needsToSave(byte fc, boolean isValid, boolean addLevel0Bigram) { - int level = fcToLevel(fc); - if (level == 0) { - if (isValid || !addLevel0Bigram) { - return false; - } - } - final int elapsedTime = fcToElapsedTime(fc); - return (elapsedTime < ELAPSED_TIME_MAX - 1 || level > 0); - } - - private static final class MathUtils { - public static final int[][] SCORE_TABLE = new int[FC_LEVEL_MAX][ELAPSED_TIME_MAX + 1]; - static { - for (int i = 0; i < FC_LEVEL_MAX; ++i) { - final float initialFreq; - if (i >= 2) { - initialFreq = FC_FREQ_MAX; - } else if (i == 1) { - initialFreq = FC_FREQ_MAX / 2; - } else if (i == 0) { - initialFreq = FC_FREQ_MAX / 4; - } else { - continue; - } - for (int j = 0; j < ELAPSED_TIME_MAX; ++j) { - final float elapsedHours = j * ELAPSED_TIME_INTERVAL_HOURS; - final float freq = initialFreq - * (float)Math.pow(initialFreq, elapsedHours / HALF_LIFE_HOURS); - final int intFreq = Math.min(FC_FREQ_MAX, Math.max(0, (int)freq)); - SCORE_TABLE[i][j] = intFreq; - } - } - } - } -} diff --git a/java/src/com/android/inputmethod/research/JsonUtils.java b/java/src/com/android/inputmethod/research/JsonUtils.java index 2beebdfae..6170b4339 100644 --- a/java/src/com/android/inputmethod/research/JsonUtils.java +++ b/java/src/com/android/inputmethod/research/JsonUtils.java @@ -91,7 +91,7 @@ import java.util.Map; jsonWriter.name("willAutoCorrect") .value(words.mWillAutoCorrect); jsonWriter.name("isPunctuationSuggestions") - .value(words.mIsPunctuationSuggestions); + .value(words.isPunctuationSuggestions()); jsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions); jsonWriter.name("isPrediction").value(words.mIsPrediction); jsonWriter.name("suggestedWords"); diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java index 6df7c1708..ffdb43c15 100644 --- a/java/src/com/android/inputmethod/research/MainLogBuffer.java +++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java @@ -20,7 +20,7 @@ import android.util.Log; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Dictionary; -import com.android.inputmethod.latin.Suggest; +import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest; import com.android.inputmethod.latin.define.ProductionFlag; import java.io.IOException; @@ -75,9 +75,7 @@ public abstract class MainLogBuffer extends FixedLogBuffer { // The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams. public static final int N_GRAM_SIZE = 2; - // TODO: Remove dependence on Suggest, and pass in Dictionary as a parameter to an appropriate - // method. - private final Suggest mSuggest; + private final DictionaryFacilitatorForSuggest mDictionaryFacilitator; @UsedForTesting private Dictionary mDictionaryForTesting; private boolean mIsStopping = false; @@ -89,11 +87,11 @@ public abstract class MainLogBuffer extends FixedLogBuffer { /* package for test */ int mNumWordsUntilSafeToSample; public MainLogBuffer(final int wordsBetweenSamples, final int numInitialWordsToIgnore, - final Suggest suggest) { + final DictionaryFacilitatorForSuggest dictionaryFacilitator) { super(N_GRAM_SIZE + wordsBetweenSamples); mNumWordsBetweenNGrams = wordsBetweenSamples; mNumWordsUntilSafeToSample = DEBUG ? 0 : numInitialWordsToIgnore; - mSuggest = suggest; + mDictionaryFacilitator = dictionaryFacilitator; } @UsedForTesting @@ -101,12 +99,14 @@ public abstract class MainLogBuffer extends FixedLogBuffer { mDictionaryForTesting = dictionary; } - private Dictionary getDictionary() { + private boolean isValidDictWord(final String word) { if (mDictionaryForTesting != null) { - return mDictionaryForTesting; + return mDictionaryForTesting.isValidWord(word); } - if (mSuggest == null || !mSuggest.hasMainDictionary()) return null; - return mSuggest.getMainDictionary(); + if (mDictionaryFacilitator != null) { + return mDictionaryFacilitator.isValidMainDictWord(word); + } + return false; } public void setIsStopping() { @@ -155,8 +155,9 @@ public abstract class MainLogBuffer extends FixedLogBuffer { } // Reload the dictionary in case it has changed (e.g., because the user has changed // languages). - final Dictionary dictionary = getDictionary(); - if (dictionary == null) { + if ((mDictionaryFacilitator == null + || !mDictionaryFacilitator.hasInitializedMainDictionary()) + && mDictionaryForTesting == null) { // Main dictionary is unavailable. Since we cannot check it, we cannot tell if a // word is out-of-vocabulary or not. Therefore, we must judge the entire buffer // contents to potentially pose a privacy risk. @@ -166,7 +167,6 @@ public abstract class MainLogBuffer extends FixedLogBuffer { // Check each word in the buffer. If any word poses a privacy threat, we cannot upload // the complete buffer contents in detail. int numWordsInLogUnitList = 0; - final int length = logUnits.size(); for (final LogUnit logUnit : logUnits) { if (!logUnit.hasOneOrMoreWords()) { // Digits outside words are a privacy threat. @@ -178,11 +178,11 @@ public abstract class MainLogBuffer extends FixedLogBuffer { final String[] words = logUnit.getWordsAsStringArray(); for (final String word : words) { // Words not in the dictionary are a privacy threat. - if (ResearchLogger.hasLetters(word) && !(dictionary.isValidWord(word))) { + if (ResearchLogger.hasLetters(word) && !isValidDictWord(word)) { if (DEBUG) { Log.d(TAG, "\"" + word + "\" NOT SAFE!: hasLetters: " + ResearchLogger.hasLetters(word) - + ", isValid: " + (dictionary.isValidWord(word))); + + ", isValid: " + isValidDictWord(word)); } return PUBLISHABILITY_UNPUBLISHABLE_NOT_IN_DICTIONARY; } diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index da9c61103..b1f54c0b4 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -52,14 +52,14 @@ import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.KeyboardView; import com.android.inputmethod.keyboard.MainKeyboardView; import com.android.inputmethod.latin.Constants; -import com.android.inputmethod.latin.Dictionary; +import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest; import com.android.inputmethod.latin.LatinIME; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.RichInputConnection; -import com.android.inputmethod.latin.Suggest; import com.android.inputmethod.latin.SuggestedWords; import com.android.inputmethod.latin.define.ProductionFlag; import com.android.inputmethod.latin.utils.InputTypeUtils; +import com.android.inputmethod.latin.utils.StringUtils; import com.android.inputmethod.latin.utils.TextRange; import com.android.inputmethod.research.MotionEventReader.ReplayData; import com.android.inputmethod.research.ui.SplashScreen; @@ -102,10 +102,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; private static final boolean DEBUG_REPLAY_AFTER_FEEDBACK = false && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; - // Whether the TextView contents are logged at the end of the session. true will disclose - // private info. - private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false - && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; // Whether the feedback dialog preserves the editable text across invocations. Should be false // for normal research builds so users do not have to delete the same feedback string they // entered earlier. Should be true for builds internal to a development team so when the text @@ -113,7 +109,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // feedback mechanism to generate multiple tests. private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false; /* package */ static boolean sIsLogging = false; - private static final int OUTPUT_FORMAT_VERSION = 5; + private static final int OUTPUT_FORMAT_VERSION = 6; // Whether all words should be recorded, leaving unsampled word between bigrams. Useful for // testing. /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false @@ -136,7 +132,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public static final String RESEARCH_KEY_OUTPUT_TEXT = ".research."; // constants related to specific log points - private static final String WHITESPACE_SEPARATORS = " \t\n\r"; + private static final int[] WHITESPACE_SEPARATORS = + StringUtils.toSortedCodePointArray(" \t\n\r"); private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel"; @@ -168,12 +165,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // U+E001 is in the "private-use area" /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001"; protected static final int SUSPEND_DURATION_IN_MINUTES = 1; - // set when LatinIME should ignore an onUpdateSelection() callback that - // arises from operations in this class - private static boolean sLatinIMEExpectingUpdateSelection = false; // used to check whether words are not unique - private Suggest mSuggest; + private DictionaryFacilitatorForSuggest mDictionaryFacilitator; private MainKeyboardView mMainKeyboardView; // TODO: Check whether a superclass can be used instead of LatinIME. /* package for test */ LatinIME mLatinIME; @@ -212,8 +206,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return sInstance; } - public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher, - final Suggest suggest) { + public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) { assert latinIME != null; mLatinIME = latinIME; mPrefs = PreferenceManager.getDefaultSharedPreferences(latinIME); @@ -249,7 +242,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang System.currentTimeMillis(), System.nanoTime()), mLatinIME); final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1); mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore, - mSuggest) { + mDictionaryFacilitator) { @Override protected void publish(final ArrayList<LogUnit> logUnits, boolean canIncludePrivateData) { @@ -262,10 +255,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang + ", cipd: " + canIncludePrivateData); } for (final String word : logUnit.getWordsAsStringArray()) { - final Dictionary dictionary = getDictionary(); + final boolean isDictionaryWord = mDictionaryFacilitator != null + && mDictionaryFacilitator.isValidMainDictWord(word); mStatistics.recordWordEntered( - dictionary != null && dictionary.isValidWord(word), - logUnit.containsUserDeletions()); + isDictionaryWord, logUnit.containsUserDeletions()); } } publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData); @@ -663,8 +656,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mInFeedbackDialog = false; } - public void initSuggest(final Suggest suggest) { - mSuggest = suggest; + public void initDictionary(final DictionaryFacilitatorForSuggest dictionaryFacilitator) { + mDictionaryFacilitator = dictionaryFacilitator; // MainLogBuffer now has an out-of-date Suggest object. Close down MainLogBuffer and create // a new one. if (mMainLogBuffer != null) { @@ -672,13 +665,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } - private Dictionary getDictionary() { - if (mSuggest == null) { - return null; - } - return mSuggest.getMainDictionary(); - } - private void setIsPasswordView(boolean isPasswordView) { mIsPasswordView = isPasswordView; } @@ -972,11 +958,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private String scrubWord(String word) { - final Dictionary dictionary = getDictionary(); - if (dictionary == null) { - return WORD_REPLACEMENT_STRING; - } - if (dictionary.isValidWord(word)) { + if (mDictionaryFacilitator != null && mDictionaryFacilitator.isValidMainDictWord(word)) { return word; } return WORD_REPLACEMENT_STRING; @@ -1126,12 +1108,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang new Object[] { applicationSpecifiedCompletions }); } - public static boolean getAndClearLatinIMEExpectingUpdateSelection() { - boolean returnValue = sLatinIMEExpectingUpdateSelection; - sLatinIMEExpectingUpdateSelection = false; - return returnValue; - } - /** * The IME is finishing; it is either being destroyed, or is about to be hidden. * @@ -1141,59 +1117,19 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private static final LogStatement LOGSTATEMENT_LATINIME_ONFINISHINPUTVIEWINTERNAL = new LogStatement("LatinIMEOnFinishInputViewInternal", false, false, "isTextTruncated", "text"); - public static void latinIME_onFinishInputViewInternal(final boolean finishingInput, - final int savedSelectionStart, final int savedSelectionEnd, final InputConnection ic) { + public static void latinIME_onFinishInputViewInternal(final boolean finishingInput) { // The finishingInput flag is set in InputMethodService. It is true if called from // doFinishInput(), which can be called as part of doStartInput(). This can happen at times // when the IME is not closing, such as when powering up. The finishinInput flag is false // if called from finishViews(), which is called from hideWindow() and onDestroy(). These // are the situations in which we want to finish up the researchLog. - if (ic != null && !finishingInput) { - final boolean isTextTruncated; - final String text; - if (LOG_FULL_TEXTVIEW_CONTENTS) { - // Capture the TextView contents. This will trigger onUpdateSelection(), so we - // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called, - // it can tell that it was generated by the logging code, and not by the user, and - // therefore keep user-visible state as is. - ic.beginBatchEdit(); - ic.performContextMenuAction(android.R.id.selectAll); - CharSequence charSequence = ic.getSelectedText(0); - if (savedSelectionStart != -1 && savedSelectionEnd != -1) { - ic.setSelection(savedSelectionStart, savedSelectionEnd); - } - ic.endBatchEdit(); - sLatinIMEExpectingUpdateSelection = true; - if (TextUtils.isEmpty(charSequence)) { - isTextTruncated = false; - text = ""; - } else { - if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) { - int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE; - // do not cut in the middle of a supplementary character - final char c = charSequence.charAt(length - 1); - if (Character.isHighSurrogate(c)) { - length--; - } - final CharSequence truncatedCharSequence = charSequence.subSequence(0, - length); - isTextTruncated = true; - text = truncatedCharSequence.toString(); - } else { - isTextTruncated = false; - text = charSequence.toString(); - } - } - } else { - isTextTruncated = true; - text = ""; - } + if (!finishingInput) { final ResearchLogger researchLogger = getInstance(); // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g. // during a live user test), so the normal isPotentiallyPrivate and // isPotentiallyRevealing flags do not apply researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONFINISHINPUTVIEWINTERNAL, - isTextTruncated, text); + true /* isTextTruncated */, "" /* text */); researchLogger.commitCurrentLogUnit(); getInstance().stop(); } @@ -1213,9 +1149,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public static void latinIME_onUpdateSelection(final int lastSelectionStart, final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd, final int newSelStart, final int newSelEnd, final int composingSpanStart, - final int composingSpanEnd, final boolean expectingUpdateSelection, - final boolean expectingUpdateSelectionFromLogger, - final RichInputConnection connection) { + final int composingSpanEnd, final RichInputConnection connection) { String word = ""; if (connection != null) { TextRange range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1); @@ -1227,8 +1161,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang final String scrubbedWord = researchLogger.scrubWord(word); researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONUPDATESELECTION, lastSelectionStart, lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd, - composingSpanStart, composingSpanEnd, expectingUpdateSelection, - expectingUpdateSelectionFromLogger, scrubbedWord); + composingSpanStart, composingSpanEnd, false /* expectingUpdateSelection */, + false /* expectingUpdateSelectionFromLogger */, scrubbedWord); } /** @@ -1411,8 +1345,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD = new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale", "orientation", "width", "modeName", "action", "navigateNext", - "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled", - "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th", + "navigatePrevious", "clobberSettingsKey", "passwordInput", + "supportsSwitchingToShortcutIme", "hasShortcutKey", "languageSwitchKeyEnabled", + "isMultiLine", "tw", "th", "keys"); public static void mainKeyboardView_setKeyboard(final Keyboard keyboard, final int orientation) { @@ -1425,7 +1360,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), orientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(), kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey, - isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey, + isPasswordView, kid.mSupportsSwitchingToShortcutIme, kid.mHasShortcutKey, kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth, keyboard.mOccupiedHeight, keyboard.getKeys()); } |