aboutsummaryrefslogtreecommitdiffstats
path: root/java/src
diff options
context:
space:
mode:
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java16
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java2
-rw-r--r--java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java20
-rw-r--r--java/src/com/android/inputmethod/compat/ActivityManagerCompatUtils.java46
-rw-r--r--java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java6
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/ActionBatch.java51
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java5
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java6
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java11
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java6
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionaryService.java84
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java25
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/EventHandler.java2
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java28
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java84
-rw-r--r--java/src/com/android/inputmethod/dictionarypack/WordListPreference.java2
-rw-r--r--java/src/com/android/inputmethod/event/EventInterpreter.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java67
-rw-r--r--java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java768
-rw-r--r--java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java90
-rw-r--r--java/src/com/android/inputmethod/keyboard/Key.java175
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyDetector.java7
-rw-r--r--java/src/com/android/inputmethod/keyboard/Keyboard.java56
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java10
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardId.java37
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java183
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java90
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardView.java71
-rw-r--r--java/src/com/android/inputmethod/keyboard/MainKeyboardView.java441
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java57
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java23
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java19
-rw-r--r--java/src/com/android/inputmethod/keyboard/PointerTracker.java250
-rw-r--r--java/src/com/android/inputmethod/keyboard/ProximityInfo.java151
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java85
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java223
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java6
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java4
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java4
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java10
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java4
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java29
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java366
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java6
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java3
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java21
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java139
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java62
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java1441
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeysCache.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java1
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java113
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java4
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java7
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/RoundedLine.java7
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java212
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java66
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java14
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/TouchScreenRegulator.java155
-rw-r--r--java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java70
-rw-r--r--java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java14
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java187
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java8
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java71
-rw-r--r--java/src/com/android/inputmethod/latin/Constants.java41
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java39
-rw-r--r--java/src/com/android/inputmethod/latin/DicTraverseSession.java12
-rw-r--r--java/src/com/android/inputmethod/latin/Dictionary.java74
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryCollection.java11
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFactory.java10
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java1
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryWriter.java105
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java671
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableDictionary.java532
-rw-r--r--java/src/com/android/inputmethod/latin/InputAttributes.java3
-rw-r--r--java/src/com/android/inputmethod/latin/InputPointers.java12
-rw-r--r--java/src/com/android/inputmethod/latin/InputView.java32
-rw-r--r--java/src/com/android/inputmethod/latin/LastComposedWord.java4
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java1188
-rw-r--r--java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java106
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java241
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputMethodManager.java61
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeSwitcher.java70
-rw-r--r--java/src/com/android/inputmethod/latin/Suggest.java143
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestedWords.java80
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java9
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java9
-rw-r--r--java/src/com/android/inputmethod/latin/UserBinaryDictionary.java50
-rw-r--r--java/src/com/android/inputmethod/latin/UserHistoryDictionary.java406
-rw-r--r--java/src/com/android/inputmethod/latin/Utils.java511
-rw-r--r--java/src/com/android/inputmethod/latin/WordComposer.java62
-rw-r--r--java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java (renamed from java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java)36
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java624
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java958
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java873
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java1755
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/DictDecoder.java394
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/DictEncoder.java38
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java502
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FormatSpec.java271
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java389
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java (renamed from java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java)6
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/SparseTable.java150
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java234
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java255
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java266
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java294
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java231
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java183
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java73
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java37
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java129
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java122
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java37
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java (renamed from java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java)12
-rw-r--r--java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java40
-rw-r--r--java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java (renamed from java/src/com/android/inputmethod/latin/AdditionalFeaturesSettingUtils.java)10
-rw-r--r--java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java (renamed from java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java)39
-rw-r--r--java/src/com/android/inputmethod/latin/settings/DebugSettings.java (renamed from java/src/com/android/inputmethod/latin/DebugSettings.java)22
-rw-r--r--java/src/com/android/inputmethod/latin/settings/DebugSettingsActivity.java (renamed from java/src/com/android/inputmethod/latin/DebugSettingsActivity.java)4
-rw-r--r--java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java (renamed from java/src/com/android/inputmethod/latin/NativeSuggestOptions.java)5
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SeekBarDialogPreference.java (renamed from java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java)43
-rw-r--r--java/src/com/android/inputmethod/latin/settings/Settings.java (renamed from java/src/com/android/inputmethod/latin/Settings.java)118
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SettingsActivity.java (renamed from java/src/com/android/inputmethod/latin/SettingsActivity.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SettingsFragment.java (renamed from java/src/com/android/inputmethod/latin/SettingsFragment.java)110
-rw-r--r--java/src/com/android/inputmethod/latin/settings/SettingsValues.java (renamed from java/src/com/android/inputmethod/latin/SettingsValues.java)122
-rw-r--r--java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java23
-rw-r--r--java/src/com/android/inputmethod/latin/setup/SetupActivity.java48
-rw-r--r--java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java43
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java31
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java2
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java18
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java4
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java4
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java8
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java2
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java107
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java37
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java30
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java24
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java63
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java24
-rw-r--r--java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java42
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java (renamed from java/src/com/android/inputmethod/latin/AdditionalSubtype.java)26
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java65
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java71
-rw-r--r--java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java (renamed from java/src/com/android/inputmethod/latin/AutoCorrection.java)12
-rw-r--r--java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java (renamed from java/src/com/android/inputmethod/latin/BoundedTreeSet.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java81
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java (renamed from java/src/com/android/inputmethod/latin/CapsModeUtils.java)16
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CollectionUtils.java (renamed from java/src/com/android/inputmethod/latin/CollectionUtils.java)7
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CompletionInfoUtils.java (renamed from java/src/com/android/inputmethod/latin/CompletionInfoUtils.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java (renamed from java/src/com/android/inputmethod/latin/CoordinateUtils.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/CsvUtils.java3
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java (renamed from java/src/com/android/inputmethod/dictionarypack/Utils.java)37
-rw-r--r--java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java (renamed from java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java)25
-rw-r--r--java/src/com/android/inputmethod/latin/utils/FeedbackUtils.java (renamed from java/src/com/android/inputmethod/latin/FeedbackUtils.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/FileTransforms.java (renamed from java/src/com/android/inputmethod/latin/FileTransforms.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/InputTypeUtils.java (renamed from java/src/com/android/inputmethod/latin/InputTypeUtils.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/IntentUtils.java (renamed from java/src/com/android/inputmethod/latin/IntentUtils.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/JniUtils.java (renamed from java/src/com/android/inputmethod/latin/JniUtils.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java77
-rw-r--r--java/src/com/android/inputmethod/latin/utils/LocaleUtils.java (renamed from java/src/com/android/inputmethod/latin/LocaleUtils.java)37
-rw-r--r--java/src/com/android/inputmethod/latin/utils/MetadataFileUriGetter.java (renamed from java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java)4
-rw-r--r--java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java147
-rw-r--r--java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java (renamed from java/src/com/android/inputmethod/latin/RecapitalizeStatus.java)9
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java (renamed from java/src/com/android/inputmethod/latin/ResizableIntArray.java)11
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ResourceUtils.java (renamed from java/src/com/android/inputmethod/latin/ResourceUtils.java)33
-rw-r--r--java/src/com/android/inputmethod/latin/utils/RunInLocale.java55
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java110
-rw-r--r--java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java (renamed from java/src/com/android/inputmethod/latin/StaticInnerHandlerWrapper.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/StringUtils.java (renamed from java/src/com/android/inputmethod/latin/StringUtils.java)158
-rw-r--r--java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java (renamed from java/src/com/android/inputmethod/latin/SubtypeLocale.java)64
-rw-r--r--java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java (renamed from java/src/com/android/inputmethod/latin/TargetPackageInfoGetterTask.java)2
-rw-r--r--java/src/com/android/inputmethod/latin/utils/TextRange.java120
-rw-r--r--java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java (renamed from java/src/com/android/inputmethod/keyboard/TypefaceUtils.java)9
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java293
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java (renamed from java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java)108
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java (renamed from java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java)21
-rw-r--r--java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java137
-rw-r--r--java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java (renamed from java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java)8
-rw-r--r--java/src/com/android/inputmethod/latin/utils/XmlParseUtils.java (renamed from java/src/com/android/inputmethod/latin/XmlParseUtils.java)2
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackActivity.java1
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackFragment.java1
-rw-r--r--java/src/com/android/inputmethod/research/JsonUtils.java19
-rw-r--r--java/src/com/android/inputmethod/research/LogUnit.java26
-rw-r--r--java/src/com/android/inputmethod/research/MainLogBuffer.java4
-rw-r--r--java/src/com/android/inputmethod/research/MotionEventReader.java10
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLog.java3
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLogger.java209
-rw-r--r--java/src/com/android/inputmethod/research/Statistics.java6
-rw-r--r--java/src/com/android/inputmethod/research/UploaderService.java5
-rw-r--r--java/src/com/android/inputmethod/research/ui/SplashScreen.java111
200 files changed, 14251 insertions, 7526 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
index 0576f666c..7639432aa 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
@@ -35,8 +35,8 @@ import android.view.inputmethod.EditorInfo;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardView;
-import com.android.inputmethod.latin.CollectionUtils;
-import com.android.inputmethod.latin.CoordinateUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.CoordinateUtils;
/**
* Exposes a virtual view sub-tree for {@link KeyboardView} and generates
@@ -157,7 +157,7 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider
// Add the virtual children of the root View.
final Keyboard keyboard = mKeyboardView.getKeyboard();
- final Key[] keys = keyboard.mKeys;
+ final Key[] keys = keyboard.getKeys();
for (Key key : keys) {
final int childVirtualViewId = generateVirtualViewIdForKey(key);
rootInfo.addChild(mKeyboardView, childVirtualViewId);
@@ -172,7 +172,7 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider
return null;
}
final String keyDescription = getKeyDescription(key);
- final Rect boundsInParent = key.mHitBox;
+ final Rect boundsInParent = key.getHitBox();
// Calculate the key's in-screen bounds.
mTempBoundsInScreen.set(boundsInParent);
@@ -208,8 +208,8 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider
* @param key The key to press.
*/
void simulateKeyPress(final Key key) {
- final int x = key.mHitBox.centerX();
- final int y = key.mHitBox.centerY();
+ final int x = key.getHitBox().centerX();
+ final int y = key.getHitBox().centerY();
final long downTime = SystemClock.uptimeMillis();
final MotionEvent downEvent = MotionEvent.obtain(
downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0);
@@ -300,7 +300,7 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider
}
mVirtualViewIdToKey.clear();
- final Key[] keys = keyboard.mKeys;
+ final Key[] keys = keyboard.getKeys();
for (Key key : keys) {
final int virtualViewId = generateVirtualViewIdForKey(key);
mVirtualViewIdToKey.put(virtualViewId, key);
@@ -325,6 +325,6 @@ public final class AccessibilityEntityProvider extends AccessibilityNodeProvider
// The key x- and y-coordinates are stable between layout changes.
// Generate an identifier by bit-shifting the x-coordinate to the
// left-half of the integer and OR'ing with the y-coordinate.
- return ((0xFFFF & key.mX) << (Integer.SIZE / 2)) | (0xFFFF & key.mY);
+ return ((0xFFFF & key.getX()) << (Integer.SIZE / 2)) | (0xFFFF & key.getY());
}
}
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
index ee52de1d1..8929dc7e9 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
@@ -33,8 +33,8 @@ import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.EditorInfo;
import com.android.inputmethod.compat.SettingsSecureCompatUtils;
-import com.android.inputmethod.latin.InputTypeUtils;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.InputTypeUtils;
public final class AccessibilityUtils {
private static final String TAG = AccessibilityUtils.class.getSimpleName();
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index 05d8269b7..58624a2e6 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -25,9 +25,9 @@ import android.view.inputmethod.EditorInfo;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardId;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.HashMap;
@@ -97,7 +97,7 @@ public final class KeyCodeDescriptionMapper {
*/
public String getDescriptionForKey(final Context context, final Keyboard keyboard,
final Key key, final boolean shouldObscure) {
- final int code = key.mCode;
+ final int code = key.getCode();
if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
@@ -116,8 +116,8 @@ public final class KeyCodeDescriptionMapper {
return getDescriptionForActionKey(context, keyboard, key);
}
- if (!TextUtils.isEmpty(key.mLabel)) {
- final String label = key.mLabel.toString().trim();
+ if (!TextUtils.isEmpty(key.getLabel())) {
+ final String label = key.getLabel().trim();
// First, attempt to map the label to a pre-defined description.
if (mKeyLabelMap.containsKey(label)) {
@@ -126,7 +126,7 @@ public final class KeyCodeDescriptionMapper {
}
// Just attempt to speak the description.
- if (key.mCode != Constants.CODE_UNSPECIFIED) {
+ if (key.getCode() != Constants.CODE_UNSPECIFIED) {
return getDescriptionForKeyCode(context, keyboard, key, shouldObscure);
}
return null;
@@ -215,8 +215,8 @@ public final class KeyCodeDescriptionMapper {
final int resId;
// Always use the label, if available.
- if (!TextUtils.isEmpty(key.mLabel)) {
- return key.mLabel.toString().trim();
+ if (!TextUtils.isEmpty(key.getLabel())) {
+ return key.getLabel().trim();
}
// Otherwise, use the action ID.
@@ -267,7 +267,7 @@ public final class KeyCodeDescriptionMapper {
*/
private String getDescriptionForKeyCode(final Context context, final Keyboard keyboard,
final Key key, final boolean shouldObscure) {
- final int code = key.mCode;
+ final int code = key.getCode();
// If the key description should be obscured, now is the time to do it.
final boolean isDefinedNonCtrl = Character.isDefined(code) && !Character.isISOControl(code);
@@ -280,8 +280,8 @@ public final class KeyCodeDescriptionMapper {
if (isDefinedNonCtrl) {
return Character.toString((char) code);
}
- if (!TextUtils.isEmpty(key.mLabel)) {
- return key.mLabel;
+ if (!TextUtils.isEmpty(key.getLabel())) {
+ return key.getLabel();
}
return context.getString(R.string.spoken_description_unknown, code);
}
diff --git a/java/src/com/android/inputmethod/compat/ActivityManagerCompatUtils.java b/java/src/com/android/inputmethod/compat/ActivityManagerCompatUtils.java
new file mode 100644
index 000000000..385e3e025
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/ActivityManagerCompatUtils.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.compat;
+
+import android.app.ActivityManager;
+import android.content.Context;
+
+import java.lang.reflect.Method;
+
+public class ActivityManagerCompatUtils {
+ private static final Object LOCK = new Object();
+ private static volatile Boolean sBoolean = null;
+ private static final Method METHOD_isLowRamDevice = CompatUtils.getMethod(
+ ActivityManager.class, "isLowRamDevice");
+
+ private ActivityManagerCompatUtils() {
+ // Do not instantiate this class.
+ }
+
+ public static boolean isLowRamDevice(Context context) {
+ if (sBoolean == null) {
+ synchronized(LOCK) {
+ if (sBoolean == null) {
+ final ActivityManager am =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ sBoolean = (Boolean)CompatUtils.invoke(am, false, METHOD_isLowRamDevice);
+ }
+ }
+ }
+ return sBoolean;
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
index 141e08a8d..55282c583 100644
--- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
+++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
@@ -23,17 +23,15 @@ import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.SuggestionSpan;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
public final class SuggestionSpanUtils {
- private static final String TAG = SuggestionSpanUtils.class.getSimpleName();
-
// Note that SuggestionSpan.FLAG_AUTO_CORRECTION has been introduced
// in API level 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1).
public static final Field FIELD_FLAG_AUTO_CORRECTION = CompatUtils.getField(
@@ -60,7 +58,7 @@ public final class SuggestionSpanUtils {
}
final Spannable spannable = new SpannableString(text);
final SuggestionSpan suggestionSpan = new SuggestionSpan(context, null /* locale */,
- new String[] {} /* suggestions */, (int)OBJ_FLAG_AUTO_CORRECTION,
+ new String[] {} /* suggestions */, OBJ_FLAG_AUTO_CORRECTION,
SuggestionSpanPickedNotificationReceiver.class);
spannable.setSpan(suggestionSpan, 0, text.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
index bf2230553..d5e638e7e 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
@@ -28,6 +28,8 @@ import android.util.Log;
import com.android.inputmethod.compat.DownloadManagerCompatUtils;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.ApplicationUtils;
+import com.android.inputmethod.latin.utils.DebugLogUtils;
import java.util.LinkedList;
import java.util.Queue;
@@ -98,7 +100,7 @@ public final class ActionBatch {
final boolean mForceStartNow;
public StartDownloadAction(final String clientId,
final WordListMetadata wordList, final boolean forceStartNow) {
- Utils.l("New download action for client ", clientId, " : ", wordList);
+ DebugLogUtils.l("New download action for client ", clientId, " : ", wordList);
mClientId = clientId;
mWordList = wordList;
mForceStartNow = forceStartNow;
@@ -110,7 +112,7 @@ public final class ActionBatch {
Log.e(TAG, "UpdateAction with a null parameter!");
return;
}
- Utils.l("Downloading word list");
+ DebugLogUtils.l("Downloading word list");
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion);
@@ -132,7 +134,7 @@ public final class ActionBatch {
+ " for an upgrade action. Fall back to download.");
}
// Download it.
- Utils.l("Upgrade word list, downloading", mWordList.mRemoteFilename);
+ DebugLogUtils.l("Upgrade word list, downloading", mWordList.mRemoteFilename);
// TODO: if DownloadManager is disabled or not installed, download by ourselves
if (null == manager) return;
@@ -142,7 +144,7 @@ public final class ActionBatch {
// DownloadManager also stupidly cuts the extension to replace with its own that it
// gets from the content-type. We need to circumvent this.
final String disambiguator = "#" + System.currentTimeMillis()
- + com.android.inputmethod.latin.Utils.getVersionName(context) + ".dict";
+ + ApplicationUtils.getVersionName(context) + ".dict";
final Uri uri = Uri.parse(mWordList.mRemoteFilename + disambiguator);
final Request request = new Request(uri);
@@ -178,7 +180,7 @@ public final class ActionBatch {
final long downloadId = UpdateHandler.registerDownloadRequest(manager, request, db,
mWordList.mId, mWordList.mVersion);
- Utils.l("Starting download of", uri, "with id", downloadId);
+ DebugLogUtils.l("Starting download of", uri, "with id", downloadId);
PrivateLog.log("Starting download of " + uri + ", id : " + downloadId);
}
}
@@ -195,7 +197,8 @@ public final class ActionBatch {
public InstallAfterDownloadAction(final String clientId,
final ContentValues wordListValues) {
- Utils.l("New InstallAfterDownloadAction for client ", clientId, " : ", wordListValues);
+ DebugLogUtils.l("New InstallAfterDownloadAction for client ", clientId, " : ",
+ wordListValues);
mClientId = clientId;
mWordListValues = wordListValues;
}
@@ -213,7 +216,7 @@ public final class ActionBatch {
+ " for an InstallAfterDownload action. Bailing out.");
return;
}
- Utils.l("Setting word list as installed");
+ DebugLogUtils.l("Setting word list as installed");
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
MetadataDbHelper.markEntryAsFinishedDownloadingAndInstalled(db, mWordListValues);
}
@@ -229,7 +232,7 @@ public final class ActionBatch {
final WordListMetadata mWordList;
public EnableAction(final String clientId, final WordListMetadata wordList) {
- Utils.l("New EnableAction for client ", clientId, " : ", wordList);
+ DebugLogUtils.l("New EnableAction for client ", clientId, " : ", wordList);
mClientId = clientId;
mWordList = wordList;
}
@@ -240,7 +243,7 @@ public final class ActionBatch {
Log.e(TAG, "EnableAction with a null parameter!");
return;
}
- Utils.l("Enabling word list");
+ DebugLogUtils.l("Enabling word list");
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion);
@@ -264,7 +267,7 @@ public final class ActionBatch {
// The word list to disable. May not be null.
final WordListMetadata mWordList;
public DisableAction(final String clientId, final WordListMetadata wordlist) {
- Utils.l("New Disable action for client ", clientId, " : ", wordlist);
+ DebugLogUtils.l("New Disable action for client ", clientId, " : ", wordlist);
mClientId = clientId;
mWordList = wordlist;
}
@@ -275,7 +278,7 @@ public final class ActionBatch {
Log.e(TAG, "DisableAction with a null word list!");
return;
}
- Utils.l("Disabling word list : " + mWordList);
+ DebugLogUtils.l("Disabling word list : " + mWordList);
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion);
@@ -311,7 +314,7 @@ public final class ActionBatch {
// The word list to make available. May not be null.
final WordListMetadata mWordList;
public MakeAvailableAction(final String clientId, final WordListMetadata wordlist) {
- Utils.l("New MakeAvailable action", clientId, " : ", wordlist);
+ DebugLogUtils.l("New MakeAvailable action", clientId, " : ", wordlist);
mClientId = clientId;
mWordList = wordlist;
}
@@ -328,7 +331,7 @@ public final class ActionBatch {
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' "
+ " for a makeavailable action. Marking as available anyway.");
}
- Utils.l("Making word list available : " + mWordList);
+ DebugLogUtils.l("Making word list available : " + mWordList);
// If mLocalFilename is null, then it's a remote file that hasn't been downloaded
// yet, so we set the local filename to the empty string.
final ContentValues values = MetadataDbHelper.makeContentValues(0,
@@ -360,7 +363,7 @@ public final class ActionBatch {
// The word list to mark pre-installed. May not be null.
final WordListMetadata mWordList;
public MarkPreInstalledAction(final String clientId, final WordListMetadata wordlist) {
- Utils.l("New MarkPreInstalled action", clientId, " : ", wordlist);
+ DebugLogUtils.l("New MarkPreInstalled action", clientId, " : ", wordlist);
mClientId = clientId;
mWordList = wordlist;
}
@@ -377,7 +380,7 @@ public final class ActionBatch {
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' "
+ " for a markpreinstalled action. Marking as preinstalled anyway.");
}
- Utils.l("Marking word list preinstalled : " + mWordList);
+ DebugLogUtils.l("Marking word list preinstalled : " + mWordList);
// This word list is pre-installed : we don't have its file. We should reset
// the local file name to the empty string so that we don't try to open it
// accidentally. The remote filename may be set by the application if it so wishes.
@@ -401,7 +404,7 @@ public final class ActionBatch {
private final String mClientId;
final WordListMetadata mWordList;
public UpdateDataAction(final String clientId, final WordListMetadata wordlist) {
- Utils.l("New UpdateData action for client ", clientId, " : ", wordlist);
+ DebugLogUtils.l("New UpdateData action for client ", clientId, " : ", wordlist);
mClientId = clientId;
mWordList = wordlist;
}
@@ -419,7 +422,7 @@ public final class ActionBatch {
Log.e(TAG, "Trying to update data about a non-existing word list. Bailing out.");
return;
}
- Utils.l("Updating data about a word list : " + mWordList);
+ DebugLogUtils.l("Updating data about a word list : " + mWordList);
final ContentValues values = MetadataDbHelper.makeContentValues(
oldValues.getAsInteger(MetadataDbHelper.PENDINGID_COLUMN),
oldValues.getAsInteger(MetadataDbHelper.TYPE_COLUMN),
@@ -453,7 +456,7 @@ public final class ActionBatch {
final boolean mHasNewerVersion;
public ForgetAction(final String clientId, final WordListMetadata wordlist,
final boolean hasNewerVersion) {
- Utils.l("New TryRemove action for client ", clientId, " : ", wordlist);
+ DebugLogUtils.l("New TryRemove action for client ", clientId, " : ", wordlist);
mClientId = clientId;
mWordList = wordlist;
mHasNewerVersion = hasNewerVersion;
@@ -465,7 +468,7 @@ public final class ActionBatch {
Log.e(TAG, "TryRemoveAction with a null word list!");
return;
}
- Utils.l("Trying to remove word list : " + mWordList);
+ DebugLogUtils.l("Trying to remove word list : " + mWordList);
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion);
@@ -525,7 +528,7 @@ public final class ActionBatch {
// The word list to delete. May not be null.
final WordListMetadata mWordList;
public StartDeleteAction(final String clientId, final WordListMetadata wordlist) {
- Utils.l("New StartDelete action for client ", clientId, " : ", wordlist);
+ DebugLogUtils.l("New StartDelete action for client ", clientId, " : ", wordlist);
mClientId = clientId;
mWordList = wordlist;
}
@@ -536,7 +539,7 @@ public final class ActionBatch {
Log.e(TAG, "StartDeleteAction with a null word list!");
return;
}
- Utils.l("Trying to delete word list : " + mWordList);
+ DebugLogUtils.l("Trying to delete word list : " + mWordList);
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion);
@@ -564,7 +567,7 @@ public final class ActionBatch {
// The word list to delete. May not be null.
final WordListMetadata mWordList;
public FinishDeleteAction(final String clientId, final WordListMetadata wordlist) {
- Utils.l("New FinishDelete action for client", clientId, " : ", wordlist);
+ DebugLogUtils.l("New FinishDelete action for client", clientId, " : ", wordlist);
mClientId = clientId;
mWordList = wordlist;
}
@@ -575,7 +578,7 @@ public final class ActionBatch {
Log.e(TAG, "FinishDeleteAction with a null word list!");
return;
}
- Utils.l("Trying to delete word list : " + mWordList);
+ DebugLogUtils.l("Trying to delete word list : " + mWordList);
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
mWordList.mId, mWordList.mVersion);
@@ -632,7 +635,7 @@ public final class ActionBatch {
* @param reporter a Reporter to send errors to.
*/
public void execute(final Context context, final ProblemReporter reporter) {
- Utils.l("Executing a batch of actions");
+ DebugLogUtils.l("Executing a batch of actions");
Queue<Action> remainingActions = mActions;
while (!remainingActions.isEmpty()) {
final Action a = remainingActions.poll();
diff --git a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
index c5aca174a..6d6c8f5c6 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ButtonSwitcher.java
@@ -47,6 +47,7 @@ public class ButtonSwitcher extends FrameLayout {
private Button mInstallButton;
private Button mCancelButton;
private Button mDeleteButton;
+ private DictionaryListInterfaceState mInterfaceState;
private OnClickListener mOnClickListener;
public ButtonSwitcher(Context context, AttributeSet attrs) {
@@ -57,9 +58,10 @@ public class ButtonSwitcher extends FrameLayout {
super(context, attrs, defStyle);
}
- public void reset() {
+ public void reset(final DictionaryListInterfaceState interfaceState) {
mStatus = NOT_INITIALIZED;
mAnimateToStatus = NOT_INITIALIZED;
+ mInterfaceState = interfaceState;
}
@Override
@@ -153,6 +155,7 @@ public class ButtonSwitcher extends FrameLayout {
private ViewPropertyAnimator animateButton(final View button, final int direction) {
final float outerX = getWidth();
final float innerX = button.getX() - button.getTranslationX();
+ mInterfaceState.removeFromCache((View)getParent());
if (ANIMATION_IN == direction) {
button.setClickable(true);
return button.animate().translationX(0);
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
index 5ad5900d4..13c07de35 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryListInterfaceState.java
@@ -18,7 +18,7 @@ package com.android.inputmethod.dictionarypack;
import android.view.View;
-import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.ArrayList;
import java.util.HashMap;
@@ -80,4 +80,8 @@ public class DictionaryListInterfaceState {
mViewCache.add(view);
return view;
}
+
+ public void removeFromCache(final View view) {
+ mViewCache.remove(view);
+ }
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java
index 69615887f..df0e3f0e1 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java
@@ -28,13 +28,13 @@ public class DictionaryPackConstants {
* The root domain for the dictionary pack, upon which authorities and actions will append
* their own distinctive strings.
*/
- private static final String DICTIONARY_DOMAIN = "com.android.inputmethod.dictionarypack";
+ private static final String DICTIONARY_DOMAIN = "com.android.inputmethod.dictionarypack.aosp";
/**
* Authority for the ContentProvider protocol.
*/
// TODO: find some way to factorize this string with the one in the resources
- public static final String AUTHORITY = DICTIONARY_DOMAIN + ".aosp";
+ public static final String AUTHORITY = DICTIONARY_DOMAIN;
/**
* The action of the intent for publishing that new dictionary data is available.
@@ -52,7 +52,14 @@ public class DictionaryPackConstants {
*/
public static final String UNKNOWN_DICTIONARY_PROVIDER_CLIENT = DICTIONARY_DOMAIN
+ ".UNKNOWN_CLIENT";
+
// In the above intents, the name of the string extra that contains the name of the client
// we want information about.
public static final String DICTIONARY_PROVIDER_CLIENT_EXTRA = "client";
+
+ /**
+ * The action of the intent to tell the dictionary provider to update now.
+ */
+ public static final String UPDATE_NOW_INTENT_ACTION = DICTIONARY_DOMAIN
+ + ".UPDATE_NOW";
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
index 4fbe16233..1d9b9991e 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
@@ -31,6 +31,7 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.DebugLogUtils;
import java.io.File;
import java.io.FileNotFoundException;
@@ -53,7 +54,6 @@ public final class DictionaryProvider extends ContentProvider {
private static final String QUERY_PARAMETER_MAY_PROMPT_USER = "mayPrompt";
private static final String QUERY_PARAMETER_TRUE = "true";
private static final String QUERY_PARAMETER_DELETE_RESULT = "result";
- private static final String QUERY_PARAMETER_SUCCESS = "success";
private static final String QUERY_PARAMETER_FAILURE = "failure";
public static final String QUERY_PARAMETER_PROTOCOL_VERSION = "protocol";
private static final int NO_MATCH = 0;
@@ -219,7 +219,7 @@ public final class DictionaryProvider extends ContentProvider {
@Override
public Cursor query(final Uri uri, final String[] projection, final String selection,
final String[] selectionArgs, final String sortOrder) {
- Utils.l("Uri =", uri);
+ DebugLogUtils.l("Uri =", uri);
PrivateLog.log("Query : " + uri);
final String clientId = getClientId(uri);
final int match = matchUri(uri);
@@ -227,7 +227,7 @@ public final class DictionaryProvider extends ContentProvider {
case DICTIONARY_V1_WHOLE_LIST:
case DICTIONARY_V2_WHOLE_LIST:
final Cursor c = MetadataDbHelper.queryDictionaries(getContext(), clientId);
- Utils.l("List of dictionaries with count", c.getCount());
+ DebugLogUtils.l("List of dictionaries with count", c.getCount());
PrivateLog.log("Returned a list of " + c.getCount() + " items");
return c;
case DICTIONARY_V2_DICT_INFO:
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
index 46bb5543a..41916b614 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
@@ -22,14 +22,15 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
-import android.text.format.DateUtils;
-import android.util.Log;
import android.widget.Toast;
import com.android.inputmethod.latin.R;
import java.util.Locale;
import java.util.Random;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
/**
* Service that handles background tasks for the dictionary provider.
@@ -49,17 +50,10 @@ import java.util.Random;
* to access, and mark the current state as such.
*/
public final class DictionaryService extends Service {
- private static final String TAG = DictionaryService.class.getName();
-
/**
* The package name, to use in the intent actions.
*/
- private static final String PACKAGE_NAME = "com.android.android.inputmethod.latin";
-
- /**
- * The action of the intent to tell the dictionary provider to update now.
- */
- private static final String UPDATE_NOW_INTENT_ACTION = PACKAGE_NAME + ".UPDATE_NOW";
+ private static final String PACKAGE_NAME = "com.android.inputmethod.latin";
/**
* The action of the date changing, used to schedule a periodic freshness check
@@ -82,36 +76,42 @@ public final class DictionaryService extends Service {
* How often, in milliseconds, we want to update the metadata. This is a
* floor value; actually, it may happen several hours later, or even more.
*/
- private static final long UPDATE_FREQUENCY = 4 * DateUtils.DAY_IN_MILLIS;
+ private static final long UPDATE_FREQUENCY = TimeUnit.DAYS.toMillis(4);
/**
* We are waked around midnight, local time. We want to wake between midnight and 6 am,
* roughly. So use a random time between 0 and this delay.
*/
- private static final int MAX_ALARM_DELAY = 6 * ((int)AlarmManager.INTERVAL_HOUR);
+ private static final int MAX_ALARM_DELAY = (int)TimeUnit.HOURS.toMillis(6);
/**
* How long we consider a "very long time". If no update took place in this time,
* the content provider will trigger an update in the background.
*/
- private static final long VERY_LONG_TIME = 14 * DateUtils.DAY_IN_MILLIS;
-
- /**
- * The last seen start Id. This must be stored because we must only call stopSelfResult() with
- * the last seen Id, or the service won't stop.
- */
- private int mLastSeenStartId;
+ private static final long VERY_LONG_TIME = TimeUnit.DAYS.toMillis(14);
/**
- * The command count. We need this because we need to not call stopSelfResult() while we still
- * have commands running.
+ * An executor that serializes tasks given to it.
*/
- private int mCommandCount;
+ private ThreadPoolExecutor mExecutor;
+ private static final int WORKER_THREAD_TIMEOUT_SECONDS = 15;
@Override
public void onCreate() {
- mLastSeenStartId = 0;
- mCommandCount = 0;
+ // By default, a thread pool executor does not timeout its core threads, so it will
+ // never kill them when there isn't any work to do any more. That would mean the service
+ // can never die! By creating it this way and calling allowCoreThreadTimeOut, we allow
+ // the single thread to time out after WORKER_THREAD_TIMEOUT_SECONDS = 15 seconds, allowing
+ // the process to be reclaimed by the system any time after that if it's not doing
+ // anything else.
+ // Executors#newSingleThreadExecutor creates a ThreadPoolExecutor but it returns the
+ // superclass ExecutorService which does not have the #allowCoreThreadTimeOut method,
+ // so we can't use that.
+ mExecutor = new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */,
+ WORKER_THREAD_TIMEOUT_SECONDS /* keepAliveTime */,
+ TimeUnit.SECONDS /* unit for keepAliveTime */,
+ new LinkedBlockingQueue<Runnable>() /* workQueue */);
+ mExecutor.allowCoreThreadTimeOut(true);
}
@Override
@@ -136,33 +136,35 @@ public final class DictionaryService extends Service {
* - Handle a finished download.
* This executes the actions that must be taken after a file (metadata or dictionary data
* has been downloaded (or failed to download).
+ * The commands that can be spun an another thread will be executed serially, in order, on
+ * a worker thread that is created on demand and terminates after a short while if there isn't
+ * any work left to do.
*/
@Override
public synchronized int onStartCommand(final Intent intent, final int flags,
final int startId) {
final DictionaryService self = this;
- mLastSeenStartId = startId;
- mCommandCount += 1;
if (SHOW_DOWNLOAD_TOAST_INTENT_ACTION.equals(intent.getAction())) {
// This is a UI action, it can't be run in another thread
showStartDownloadingToast(this, LocaleUtils.constructLocaleFromString(
intent.getStringExtra(LOCALE_INTENT_ARGUMENT)));
} else {
- // If it's a command that does not require UI, create a thread to do the work
- // and return right away. DATE_CHANGED or UPDATE_NOW are examples of such commands.
- new Thread("updateOrFinishDownload") {
+ // If it's a command that does not require UI, arrange for the work to be done on a
+ // separate thread, so that we can return right away. The executor will spawn a thread
+ // if necessary, or reuse a thread that has become idle as appropriate.
+ // DATE_CHANGED or UPDATE_NOW are examples of commands that can be done on another
+ // thread.
+ mExecutor.submit(new Runnable() {
@Override
public void run() {
dispatchBroadcast(self, intent);
- synchronized(self) {
- if (--mCommandCount <= 0) {
- if (!stopSelfResult(mLastSeenStartId)) {
- Log.e(TAG, "Can't stop ourselves");
- }
- }
- }
+ // Since calls to onStartCommand are serialized, the submissions to the executor
+ // are serialized. That means we are guaranteed to call the stopSelfResult()
+ // in the same order that we got them, so we don't need to take care of the
+ // order.
+ stopSelfResult(startId);
}
- }.start();
+ });
}
return Service.START_REDELIVER_INTENT;
}
@@ -173,9 +175,9 @@ public final class DictionaryService extends Service {
// at midnight local time, but it may happen if the user changes the date
// by hand or something similar happens.
checkTimeAndMaybeSetupUpdateAlarm(context);
- } else if (UPDATE_NOW_INTENT_ACTION.equals(intent.getAction())) {
+ } else if (DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION.equals(intent.getAction())) {
// Intent to trigger an update now.
- UpdateHandler.update(context, false);
+ UpdateHandler.tryUpdate(context, false);
} else {
UpdateHandler.downloadFinished(context, intent);
}
@@ -196,7 +198,7 @@ public final class DictionaryService extends Service {
// It doesn't matter too much if this is very inexact.
final long now = System.currentTimeMillis();
final long alarmTime = now + new Random().nextInt(MAX_ALARM_DELAY);
- final Intent updateIntent = new Intent(DictionaryService.UPDATE_NOW_INTENT_ACTION);
+ final Intent updateIntent = new Intent(DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION);
final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
updateIntent, PendingIntent.FLAG_CANCEL_CURRENT);
@@ -226,7 +228,7 @@ public final class DictionaryService extends Service {
*/
public static void updateNowIfNotUpdatedInAVeryLongTime(final Context context) {
if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME)) return;
- UpdateHandler.update(context, false);
+ UpdateHandler.tryUpdate(context, false);
}
/**
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
index 4b89d20bb..7bbd041e7 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
@@ -30,6 +30,7 @@ import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceGroup;
+import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.animation.AnimationUtils;
@@ -104,9 +105,16 @@ public final class DictionarySettingsFragment extends PreferenceFragment
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
- mUpdateNowMenu = menu.add(Menu.NONE, MENU_UPDATE_NOW, 0, R.string.check_for_updates_now);
- mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
- refreshNetworkState();
+ final String metadataUri =
+ MetadataDbHelper.getMetadataUriAsString(getActivity(), mClientId);
+ // We only add the "Refresh" button if we have a non-empty URL to refresh from. If the
+ // URL is empty, of course we can't refresh so it makes no sense to display this.
+ if (!TextUtils.isEmpty(metadataUri)) {
+ mUpdateNowMenu =
+ menu.add(Menu.NONE, MENU_UPDATE_NOW, 0, R.string.check_for_updates_now);
+ mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ refreshNetworkState();
+ }
}
@Override
@@ -353,7 +361,12 @@ public final class DictionarySettingsFragment extends PreferenceFragment
new Thread("updateByHand") {
@Override
public void run() {
- UpdateHandler.update(activity, true);
+ // We call tryUpdate(), which returns whether we could successfully start an update.
+ // If we couldn't, we'll never receive the end callback, so we stop the loading
+ // animation and return to the previous screen.
+ if (!UpdateHandler.tryUpdate(activity, true)) {
+ stopLoadingAnimation();
+ }
}
}.start();
}
@@ -368,7 +381,9 @@ public final class DictionarySettingsFragment extends PreferenceFragment
private void startLoadingAnimation() {
mLoadingView.setVisibility(View.VISIBLE);
getView().setVisibility(View.GONE);
- mUpdateNowMenu.setTitle(R.string.cancel);
+ // We come here when the menu element is pressed so presumably it can't be null. But
+ // better safe than sorry.
+ if (null != mUpdateNowMenu) mUpdateNowMenu.setTitle(R.string.cancel);
}
private void stopLoadingAnimation() {
diff --git a/java/src/com/android/inputmethod/dictionarypack/EventHandler.java b/java/src/com/android/inputmethod/dictionarypack/EventHandler.java
index d8aa33bb8..859f1b35b 100644
--- a/java/src/com/android/inputmethod/dictionarypack/EventHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/EventHandler.java
@@ -21,8 +21,6 @@ import android.content.Context;
import android.content.Intent;
public final class EventHandler extends BroadcastReceiver {
- private static final String TAG = EventHandler.class.getName();
-
/**
* Receives a intent broadcast.
*
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
index dac12137d..ff5aba6d8 100644
--- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
@@ -25,6 +25,7 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.DebugLogUtils;
import java.io.File;
import java.util.ArrayList;
@@ -198,6 +199,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
final ContentValues defaultMetadataValues = new ContentValues();
defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, "");
defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri);
+ defaultMetadataValues.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
db.insert(CLIENT_TABLE_NAME, null, defaultMetadataValues);
}
}
@@ -357,21 +359,21 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
}
/**
- * Get the metadata download ID for a client ID.
+ * Get the metadata download ID for a metadata URI.
*
- * This will retrieve the download ID for the metadata file associated with a client ID.
- * If there is no metadata download in progress for this client, it will return NOT_AN_ID.
+ * This will retrieve the download ID for the metadata file that has the passed URI.
+ * If this URI is not being downloaded right now, it will return NOT_AN_ID.
*
* @param context a context instance to open the database on
- * @param clientId the client ID to retrieve the metadata download ID of
+ * @param uri the URI to retrieve the metadata download ID of
* @return the metadata download ID, or NOT_AN_ID if no download is in progress
*/
- public static long getMetadataDownloadIdForClient(final Context context,
- final String clientId) {
+ public static long getMetadataDownloadIdForURI(final Context context,
+ final String uri) {
SQLiteDatabase defaultDb = getDb(context, null);
final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME,
new String[] { CLIENT_PENDINGID_COLUMN },
- CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId },
+ CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri },
null, null, null, null);
try {
if (!cursor.moveToFirst()) return UpdateHandler.NOT_AN_ID;
@@ -772,15 +774,17 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri
|| null == valuesMetadataAdditionalId) {
// We need all these columns to be filled in
- Utils.l("Missing parameter for updateClientInfo");
+ DebugLogUtils.l("Missing parameter for updateClientInfo");
return;
}
if (!clientId.equals(valuesClientId)) {
// Mismatch! The client violates the protocol.
- Utils.l("Received an updateClientInfo request for ", clientId, " but the values "
- + "contain a different ID : ", valuesClientId);
+ DebugLogUtils.l("Received an updateClientInfo request for ", clientId,
+ " but the values " + "contain a different ID : ", valuesClientId);
return;
}
+ // Default value for a pending ID is NOT_AN_ID
+ values.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID);
final SQLiteDatabase defaultDb = getDb(context, "");
if (-1 == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) {
defaultDb.update(CLIENT_TABLE_NAME, values,
@@ -847,7 +851,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
final ContentValues r) {
switch (r.getAsInteger(TYPE_COLUMN)) {
case TYPE_BULK:
- Utils.l("Ended processing a wordlist");
+ DebugLogUtils.l("Ended processing a wordlist");
// Updating a bulk word list is a three-step operation:
// - Add the new entry to the table
// - Remove the old entry from the table
@@ -869,7 +873,7 @@ public class MetadataDbHelper extends SQLiteOpenHelper {
// the phone is suddenly cut during an update.
final int filenameIndex = c.getColumnIndex(LOCAL_FILENAME_COLUMN);
do {
- Utils.l("Setting for removal", c.getString(filenameIndex));
+ DebugLogUtils.l("Setting for removal", c.getString(filenameIndex));
filenames.add(c.getString(filenameIndex));
} while (c.moveToNext());
}
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index 3f917f13f..0e7c3bb7e 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -38,6 +38,8 @@ import android.util.Log;
import com.android.inputmethod.compat.ConnectivityManagerCompatUtils;
import com.android.inputmethod.compat.DownloadManagerCompatUtils;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.ApplicationUtils;
+import com.android.inputmethod.latin.utils.DebugLogUtils;
import java.io.File;
import java.io.FileInputStream;
@@ -171,25 +173,27 @@ public final class UpdateHandler {
* Download latest metadata from the server through DownloadManager for all known clients
* @param context The context for retrieving resources
* @param updateNow Whether we should update NOW, or respect bandwidth policies
+ * @return true if an update successfully started, false otherwise.
*/
- public static void update(final Context context, final boolean updateNow) {
+ public static boolean tryUpdate(final Context context, final boolean updateNow) {
// TODO: loop through all clients instead of only doing the default one.
final TreeSet<String> uris = new TreeSet<String>();
final Cursor cursor = MetadataDbHelper.queryClientIds(context);
- if (null == cursor) return;
+ if (null == cursor) return false;
try {
- if (!cursor.moveToFirst()) return;
+ if (!cursor.moveToFirst()) return false;
do {
final String clientId = cursor.getString(0);
final String metadataUri =
MetadataDbHelper.getMetadataUriAsString(context, clientId);
- PrivateLog.log("Update for clientId " + Utils.s(clientId));
- Utils.l("Update for clientId", clientId, " which uses URI ", metadataUri);
+ PrivateLog.log("Update for clientId " + DebugLogUtils.s(clientId));
+ DebugLogUtils.l("Update for clientId", clientId, " which uses URI ", metadataUri);
uris.add(metadataUri);
} while (cursor.moveToNext());
} finally {
cursor.close();
}
+ boolean started = false;
for (final String metadataUri : uris) {
if (!TextUtils.isEmpty(metadataUri)) {
// If the metadata URI is empty, that means we should never update it at all.
@@ -198,8 +202,10 @@ public final class UpdateHandler {
// is a bug and it happens anyway, doing nothing is the right thing to do.
// For more information, {@see DictionaryProvider#insert(Uri, ContentValues)}.
updateClientsWithMetadataUri(context, updateNow, metadataUri);
+ started = true;
}
}
+ return started;
}
/**
@@ -211,14 +217,14 @@ public final class UpdateHandler {
*/
private static void updateClientsWithMetadataUri(final Context context,
final boolean updateNow, final String metadataUri) {
- PrivateLog.log("Update for metadata URI " + Utils.s(metadataUri));
+ PrivateLog.log("Update for metadata URI " + DebugLogUtils.s(metadataUri));
// 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
// gets from the content-type. We need to circumvent this.
final String disambiguator = "#" + System.currentTimeMillis()
- + com.android.inputmethod.latin.Utils.getVersionName(context) + ".json";
+ + ApplicationUtils.getVersionName(context) + ".json";
final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator));
- Utils.l("Request =", metadataRequest);
+ DebugLogUtils.l("Request =", metadataRequest);
final Resources res = context.getResources();
// By default, download over roaming is allowed and all network types are allowed too.
@@ -254,7 +260,7 @@ public final class UpdateHandler {
final long downloadId;
synchronized (sSharedIdProtector) {
downloadId = manager.enqueue(metadataRequest);
- Utils.l("Metadata download requested with id", downloadId);
+ DebugLogUtils.l("Metadata download requested with id", downloadId);
// If there is already a download in progress, it's been there for a while and
// there is probably something wrong with download manager. It's best to just
// overwrite the id and request it again. If the old one happens to finish
@@ -266,23 +272,22 @@ public final class UpdateHandler {
}
/**
- * Cancels a pending update, if there is one.
+ * Cancels downloading a file, if there is one for this URI.
*
- * If none, this is a no-op.
+ * If we are not currently downloading the file at this URI, this is a no-op.
*
* @param context the context to open the database on
- * @param clientId the id of the client
+ * @param metadataUri the URI to cancel
* @param manager an instance of DownloadManager
*/
private static void cancelUpdateWithDownloadManager(final Context context,
- final String clientId, final DownloadManager manager) {
+ final String metadataUri, final DownloadManager manager) {
synchronized (sSharedIdProtector) {
final long metadataDownloadId =
- MetadataDbHelper.getMetadataDownloadIdForClient(context, clientId);
+ MetadataDbHelper.getMetadataDownloadIdForURI(context, metadataUri);
if (NOT_AN_ID == metadataDownloadId) return;
manager.remove(metadataDownloadId);
- writeMetadataDownloadId(context,
- MetadataDbHelper.getMetadataUriAsString(context, clientId), NOT_AN_ID);
+ writeMetadataDownloadId(context, metadataUri, NOT_AN_ID);
}
// Consider a cancellation as a failure. As such, inform listeners that the download
// has failed.
@@ -292,10 +297,10 @@ public final class UpdateHandler {
}
/**
- * Cancels a pending update, if there is one.
+ * Cancels a pending update for this client, if there is one.
*
- * If there is none, this is a no-op. This is a helper method that gets the
- * download manager service.
+ * If we are not currently updating metadata for this client, this is a no-op. This is a helper
+ * method that gets the download manager service and the metadata URI for this client.
*
* @param context the context, to get an instance of DownloadManager
* @param clientId the ID of the client we want to cancel the update of
@@ -303,7 +308,8 @@ public final class UpdateHandler {
public static void cancelUpdate(final Context context, final String clientId) {
final DownloadManager manager =
(DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
- if (null != manager) cancelUpdateWithDownloadManager(context, clientId, manager);
+ final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId);
+ if (null != manager) cancelUpdateWithDownloadManager(context, metadataUri, manager);
}
/**
@@ -326,11 +332,11 @@ public final class UpdateHandler {
*/
public static long registerDownloadRequest(final DownloadManager manager, final Request request,
final SQLiteDatabase db, final String id, final int version) {
- Utils.l("RegisterDownloadRequest for word list id : ", id, ", version ", version);
+ DebugLogUtils.l("RegisterDownloadRequest for word list id : ", id, ", version ", version);
final long downloadId;
synchronized (sSharedIdProtector) {
downloadId = manager.enqueue(request);
- Utils.l("Download requested with id", downloadId);
+ DebugLogUtils.l("Download requested with id", downloadId);
MetadataDbHelper.markEntryAsDownloading(db, id, version, downloadId);
}
return downloadId;
@@ -416,7 +422,7 @@ public final class UpdateHandler {
// Get and check the ID of the file that was downloaded
final long fileId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, NOT_AN_ID);
PrivateLog.log("Download finished with id " + fileId);
- Utils.l("DownloadFinished with id", fileId);
+ DebugLogUtils.l("DownloadFinished with id", fileId);
if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore
final DownloadManager manager =
@@ -426,7 +432,7 @@ public final class UpdateHandler {
final ArrayList<DownloadRecord> recordList =
getDownloadRecordsForCompletedDownloadInfo(context, downloadInfo);
if (null == recordList) return; // It was someone else's download.
- Utils.l("Received result for download ", fileId);
+ DebugLogUtils.l("Received result for download ", fileId);
// TODO: handle gracefully a null pointer here. This is practically impossible because
// we come here only when DownloadManager explicitly called us when it ended a
@@ -503,7 +509,7 @@ public final class UpdateHandler {
private static void publishUpdateCycleCompletedEvent(final Context context) {
// Even if this is not successful, we have to publish the new state.
PrivateLog.log("Publishing update cycle completed event");
- Utils.l("Publishing update cycle completed event");
+ DebugLogUtils.l("Publishing update cycle completed event");
for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) {
listener.updateCycleCompleted();
}
@@ -517,12 +523,12 @@ public final class UpdateHandler {
// {@link handleWordList(Context,InputStream,ContentValues)}.
// Handle the downloaded file according to its type
if (downloadRecord.isMetadata()) {
- Utils.l("Data D/L'd is metadata for", downloadRecord.mClientId);
+ DebugLogUtils.l("Data D/L'd is metadata for", downloadRecord.mClientId);
// #handleMetadata() closes its InputStream argument
handleMetadata(context, new ParcelFileDescriptor.AutoCloseInputStream(
manager.openDownloadedFile(fileId)), downloadRecord.mClientId);
} else {
- Utils.l("Data D/L'd is a word list");
+ DebugLogUtils.l("Data D/L'd is a word list");
final int wordListStatus = downloadRecord.mAttributes.getAsInteger(
MetadataDbHelper.STATUS_COLUMN);
if (MetadataDbHelper.STATUS_DOWNLOADING == wordListStatus) {
@@ -582,7 +588,7 @@ public final class UpdateHandler {
*/
private static void handleMetadata(final Context context, final InputStream stream,
final String clientId) throws IOException, BadFormatException {
- Utils.l("Entering handleMetadata");
+ DebugLogUtils.l("Entering handleMetadata");
final List<WordListMetadata> newMetadata;
final InputStreamReader reader = new InputStreamReader(stream);
try {
@@ -592,7 +598,7 @@ public final class UpdateHandler {
reader.close();
}
- Utils.l("Downloaded metadata :", newMetadata);
+ DebugLogUtils.l("Downloaded metadata :", newMetadata);
PrivateLog.log("Downloaded metadata\n" + newMetadata);
final ActionBatch actions = computeUpgradeTo(context, clientId, newMetadata);
@@ -617,7 +623,7 @@ public final class UpdateHandler {
// DownloadManager does not have the ability to put the file directly where we want
// it, so we had it download to a temporary place. Now we move it. It will be deleted
// automatically by DownloadManager.
- Utils.l("Downloaded a new word list :", downloadRecord.mAttributes.getAsString(
+ DebugLogUtils.l("Downloaded a new word list :", downloadRecord.mAttributes.getAsString(
MetadataDbHelper.DESCRIPTION_COLUMN), "for", downloadRecord.mClientId);
PrivateLog.log("Downloaded a new word list with description : "
+ downloadRecord.mAttributes.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN)
@@ -676,9 +682,9 @@ public final class UpdateHandler {
*/
private static void copyFile(final InputStream in, final OutputStream out)
throws IOException {
- Utils.l("Copying files");
+ DebugLogUtils.l("Copying files");
if (!(in instanceof FileInputStream) || !(out instanceof FileOutputStream)) {
- Utils.l("Not the right types");
+ DebugLogUtils.l("Not the right types");
copyFileFallback(in, out);
} else {
try {
@@ -687,7 +693,7 @@ public final class UpdateHandler {
sourceChannel.transferTo(0, Integer.MAX_VALUE, destinationChannel);
} catch (IOException e) {
// Can't work with channels, or something went wrong. Copy by hand.
- Utils.l("Won't work");
+ DebugLogUtils.l("Won't work");
copyFileFallback(in, out);
}
}
@@ -702,7 +708,7 @@ public final class UpdateHandler {
*/
private static void copyFileFallback(final InputStream in, final OutputStream out)
throws IOException {
- Utils.l("Falling back to slow copy");
+ DebugLogUtils.l("Falling back to slow copy");
final byte[] buffer = new byte[FILE_COPY_BUFFER_SIZE];
for (int readBytes = in.read(buffer); readBytes >= 0; readBytes = in.read(buffer))
out.write(buffer, 0, readBytes);
@@ -717,10 +723,10 @@ public final class UpdateHandler {
*/
private static String getTempFileName(final Context context, final String locale)
throws IOException {
- Utils.l("Entering openTempFileOutput");
+ DebugLogUtils.l("Entering openTempFileOutput");
final File dir = context.getFilesDir();
final File f = File.createTempFile(locale + "___", DICT_FILE_SUFFIX, dir);
- Utils.l("File name is", f.getName());
+ DebugLogUtils.l("File name is", f.getName());
return f.getName();
}
@@ -741,7 +747,7 @@ public final class UpdateHandler {
final String clientId, List<WordListMetadata> from, List<WordListMetadata> to) {
final ActionBatch actions = new ActionBatch();
// Upgrade existing word lists
- Utils.l("Comparing dictionaries");
+ DebugLogUtils.l("Comparing dictionaries");
final Set<String> wordListIds = new TreeSet<String>();
// TODO: Can these be null?
if (null == from) from = new ArrayList<WordListMetadata>();
@@ -756,7 +762,7 @@ public final class UpdateHandler {
final WordListMetadata newInfo = null == metadataInfo
|| metadataInfo.mFormatVersion > MAXIMUM_SUPPORTED_FORMAT_VERSION
? null : metadataInfo;
- Utils.l("Considering updating ", id, "currentInfo =", currentInfo);
+ DebugLogUtils.l("Considering updating ", id, "currentInfo =", currentInfo);
if (null == currentInfo && null == newInfo) {
// This may happen if a new word list appeared that we can't handle.
@@ -767,7 +773,7 @@ public final class UpdateHandler {
// We may come here if there is a new word list that we can't handle.
Log.i(TAG, "Can't handle word list with id '" + id + "' because it has format"
+ " version " + metadataInfo.mFormatVersion + " and the maximum version"
- + "we can handle is " + MAXIMUM_SUPPORTED_FORMAT_VERSION);
+ + " we can handle is " + MAXIMUM_SUPPORTED_FORMAT_VERSION);
}
continue;
} else if (null == currentInfo) {
diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
index 7ec7e9c13..ba1fce1a8 100644
--- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
+++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
@@ -224,7 +224,7 @@ public final class WordListPreference extends Preference {
(ButtonSwitcher)view.findViewById(R.id.wordlist_button_switcher);
// We need to clear the state of the button switcher, because we reuse views; if we didn't
// reset it would animate from whatever its old state was.
- buttonSwitcher.reset();
+ buttonSwitcher.reset(mInterfaceState);
if (mInterfaceState.isOpen(mWordlistId)) {
// The button is open.
final int previousStatus = mInterfaceState.getStatus(mWordlistId);
diff --git a/java/src/com/android/inputmethod/event/EventInterpreter.java b/java/src/com/android/inputmethod/event/EventInterpreter.java
index 6efe899bb..726b9206b 100644
--- a/java/src/com/android/inputmethod/event/EventInterpreter.java
+++ b/java/src/com/android/inputmethod/event/EventInterpreter.java
@@ -19,9 +19,9 @@ package com.android.inputmethod.event;
import android.util.SparseArray;
import android.view.KeyEvent;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.LatinIME;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.ArrayList;
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java b/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java
new file mode 100644
index 000000000..fed134eb9
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/EmojiCategoryPageIndicatorView.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard;
+
+import com.android.inputmethod.latin.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+public class EmojiCategoryPageIndicatorView extends LinearLayout {
+ private static final float BOTTOM_MARGIN_RATIO = 0.66f;
+ private final Paint mPaint = new Paint();
+ private int mCategoryPageSize = 0;
+ private int mCurrentCategoryPageId = 0;
+ private float mOffset = 0.0f;
+
+ public EmojiCategoryPageIndicatorView(Context context) {
+ this(context, null /* attrs */);
+ }
+
+ public EmojiCategoryPageIndicatorView(Context context, 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) {
+ mCategoryPageSize = size;
+ mCurrentCategoryPageId = id;
+ mOffset = offset;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mCategoryPageSize == 0) {
+ // If the category is not set yet, just clear and return.
+ canvas.drawColor(0);
+ return;
+ }
+ final float height = getHeight();
+ final float width = getWidth();
+ final float unitWidth = width / mCategoryPageSize;
+ final float left = unitWidth * mCurrentCategoryPageId + mOffset * unitWidth;
+ final float top = 0.0f;
+ final float right = left + unitWidth;
+ final float bottom = height * BOTTOM_MARGIN_RATIO;
+ canvas.drawRect(left, top, right, bottom, mPaint);
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
new file mode 100644
index 000000000..61dc56ed1
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/EmojiKeyboardView.java
@@ -0,0 +1,768 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard;
+
+import static com.android.inputmethod.latin.Constants.NOT_A_COORDINATE;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Build;
+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.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TabHost;
+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.latin.Constants;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.settings.Settings;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.ResourceUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * View class to implement Emoji keyboards.
+ * The Emoji keyboard consists of group of views {@link R.layout#emoji_keyboard_view}.
+ * <ol>
+ * <li> Emoji category tabs.
+ * <li> Delete button.
+ * <li> Emoji keyboard pages that can be scrolled by swiping horizontally or by selecting a tab.
+ * <li> Back to main keyboard button and enter button.
+ * </ol>
+ * Because of the above reasons, this class doesn't extend {@link KeyboardView}.
+ */
+public final class EmojiKeyboardView extends LinearLayout implements OnTabChangeListener,
+ ViewPager.OnPageChangeListener, View.OnClickListener,
+ ScrollKeyboardView.OnKeyClickListener {
+ private static final String TAG = EmojiKeyboardView.class.getSimpleName();
+ private final int mKeyBackgroundId;
+ private final int mEmojiFunctionalKeyBackgroundId;
+ private final KeyboardLayoutSet mLayoutSet;
+ private final ColorStateList mTabLabelColor;
+ private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener;
+ private EmojiKeyboardAdapter mEmojiKeyboardAdapter;
+
+ private TabHost mTabHost;
+ private ViewPager mEmojiPager;
+ private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView;
+
+ private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
+
+ private static final int CATEGORY_ID_UNSPECIFIED = -1;
+ public static final int CATEGORY_ID_RECENTS = 0;
+ public static final int CATEGORY_ID_PEOPLE = 1;
+ public static final int CATEGORY_ID_OBJECTS = 2;
+ public static final int CATEGORY_ID_NATURE = 3;
+ public static final int CATEGORY_ID_PLACES = 4;
+ public static final int CATEGORY_ID_SYMBOLS = 5;
+ public static final int CATEGORY_ID_EMOTICONS = 6;
+
+ private static class CategoryProperties {
+ public int mCategoryId;
+ public int mPageCount;
+ public CategoryProperties(final int categoryId, final int pageCount) {
+ mCategoryId = categoryId;
+ mPageCount = pageCount;
+ }
+ }
+
+ private static class EmojiCategory {
+ private static final String[] sCategoryName = {
+ "recents",
+ "people",
+ "objects",
+ "nature",
+ "places",
+ "symbols",
+ "emoticons" };
+ private static final int[] sCategoryIcon = new int[] {
+ R.drawable.ic_emoji_recent_light,
+ R.drawable.ic_emoji_people_light,
+ R.drawable.ic_emoji_objects_light,
+ R.drawable.ic_emoji_nature_light,
+ R.drawable.ic_emoji_places_light,
+ R.drawable.ic_emoji_symbols_light,
+ 0 };
+ private static final String[] sCategoryLabel =
+ { null, null, null, null, null, null, ":-)" };
+ private static final int[] sCategoryElementId = {
+ KeyboardId.ELEMENT_EMOJI_RECENTS,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY1,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY2,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY3,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY4,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY5,
+ KeyboardId.ELEMENT_EMOJI_CATEGORY6 };
+ private final SharedPreferences mPrefs;
+ private final int mMaxPageKeyCount;
+ private final KeyboardLayoutSet mLayoutSet;
+ private final HashMap<String, Integer> mCategoryNameToIdMap = CollectionUtils.newHashMap();
+ private final ArrayList<CategoryProperties> mShownCategories =
+ CollectionUtils.newArrayList();
+ private final ConcurrentHashMap<Long, DynamicGridKeyboard>
+ mCategoryKeyboardMap = new ConcurrentHashMap<Long, DynamicGridKeyboard>();
+
+ private int mCurrentCategoryId = CATEGORY_ID_UNSPECIFIED;
+ private int mCurrentCategoryPageId = 0;
+
+ public EmojiCategory(final SharedPreferences prefs, final Resources res,
+ final KeyboardLayoutSet layoutSet) {
+ mPrefs = prefs;
+ mMaxPageKeyCount = res.getInteger(R.integer.emoji_keyboard_max_key_count);
+ mLayoutSet = layoutSet;
+ for (int i = 0; i < sCategoryName.length; ++i) {
+ mCategoryNameToIdMap.put(sCategoryName[i], i);
+ }
+ addShownCategoryId(CATEGORY_ID_RECENTS);
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2
+ || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KeyLimePie")
+ || android.os.Build.VERSION.CODENAME.equalsIgnoreCase("KitKat")) {
+ addShownCategoryId(CATEGORY_ID_PEOPLE);
+ addShownCategoryId(CATEGORY_ID_OBJECTS);
+ addShownCategoryId(CATEGORY_ID_NATURE);
+ addShownCategoryId(CATEGORY_ID_PLACES);
+ mCurrentCategoryId = CATEGORY_ID_PEOPLE;
+ } else {
+ mCurrentCategoryId = CATEGORY_ID_SYMBOLS;
+ }
+ addShownCategoryId(CATEGORY_ID_SYMBOLS);
+ addShownCategoryId(CATEGORY_ID_EMOTICONS);
+ getKeyboard(CATEGORY_ID_RECENTS, 0 /* cagetoryPageId */)
+ .loadRecentKeys(mCategoryKeyboardMap.values());
+ }
+
+ private void addShownCategoryId(int categoryId) {
+ // Load a keyboard of categoryId
+ getKeyboard(categoryId, 0 /* cagetoryPageId */);
+ final CategoryProperties properties =
+ new CategoryProperties(categoryId, getCategoryPageCount(categoryId));
+ mShownCategories.add(properties);
+ }
+
+ public String getCategoryName(int categoryId, int categoryPageId) {
+ return sCategoryName[categoryId] + "-" + categoryPageId;
+ }
+
+ public int getCategoryId(String name) {
+ final String[] strings = name.split("-");
+ return mCategoryNameToIdMap.get(strings[0]);
+ }
+
+ public int getCategoryIcon(int categoryId) {
+ return sCategoryIcon[categoryId];
+ }
+
+ public String getCategoryLabel(int categoryId) {
+ return sCategoryLabel[categoryId];
+ }
+
+ public ArrayList<CategoryProperties> getShownCategories() {
+ return mShownCategories;
+ }
+
+ public int getCurrentCategoryId() {
+ return mCurrentCategoryId;
+ }
+
+ public int getCurrentCategoryPageSize() {
+ return getCategoryPageSize(mCurrentCategoryId);
+ }
+
+ public int getCategoryPageSize(int categoryId) {
+ for (final CategoryProperties prop : mShownCategories) {
+ if (prop.mCategoryId == categoryId) {
+ return prop.mPageCount;
+ }
+ }
+ Log.w(TAG, "Invalid category id: " + categoryId);
+ // Should not reach here.
+ return 0;
+ }
+
+ public void setCurrentCategoryId(int categoryId) {
+ mCurrentCategoryId = categoryId;
+ }
+
+ public void setCurrentCategoryPageId(int id) {
+ mCurrentCategoryPageId = id;
+ }
+
+ public int getCurrentCategoryPageId() {
+ return mCurrentCategoryPageId;
+ }
+
+ public void saveLastTypedCategoryPage() {
+ Settings.writeEmojiCategoryLastTypedId(
+ mPrefs, mCurrentCategoryId, mCurrentCategoryPageId);
+ }
+
+ public boolean isInRecentTab() {
+ return mCurrentCategoryId == CATEGORY_ID_RECENTS;
+ }
+
+ public int getTabIdFromCategoryId(int categoryId) {
+ for (int i = 0; i < mShownCategories.size(); ++i) {
+ if (mShownCategories.get(i).mCategoryId == categoryId) {
+ return i;
+ }
+ }
+ Log.w(TAG, "categoryId not found: " + categoryId);
+ return 0;
+ }
+
+ // Returns the view pager's page position for the categoryId
+ public int getPageIdFromCategoryId(int categoryId) {
+ final int lastSavedCategoryPageId =
+ Settings.readEmojiCategoryLastTypedId(mPrefs, categoryId);
+ int sum = 0;
+ for (int i = 0; i < mShownCategories.size(); ++i) {
+ final CategoryProperties props = mShownCategories.get(i);
+ if (props.mCategoryId == categoryId) {
+ return sum + lastSavedCategoryPageId;
+ }
+ sum += props.mPageCount;
+ }
+ Log.w(TAG, "categoryId not found: " + categoryId);
+ return 0;
+ }
+
+ public int getRecentTabId() {
+ return getTabIdFromCategoryId(CATEGORY_ID_RECENTS);
+ }
+
+ private int getCategoryPageCount(int categoryId) {
+ final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
+ return (keyboard.getKeys().length - 1) / mMaxPageKeyCount + 1;
+ }
+
+ // Returns a pair of the category id and the category page id from the view pager's page
+ // 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) {
+ int sum = 0;
+ for (CategoryProperties properties : mShownCategories) {
+ final int temp = sum;
+ sum += properties.mPageCount;
+ if (sum > position) {
+ return new Pair<Integer, Integer>(properties.mCategoryId, position - temp);
+ }
+ }
+ return null;
+ }
+
+ // Returns a keyboard from the view pager's page position.
+ public DynamicGridKeyboard getKeyboardFromPagePosition(int position) {
+ final Pair<Integer, Integer> categoryAndId =
+ getCategoryIdAndPageIdFromPagePosition(position);
+ if (categoryAndId != null) {
+ return getKeyboard(categoryAndId.first, categoryAndId.second);
+ }
+ 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);
+ }
+ kbd = mCategoryKeyboardMap.get(key);
+ } else {
+ kbd = new DynamicGridKeyboard(mPrefs,
+ mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
+ mMaxPageKeyCount, categoryId, 0 /* categoryPageId */);
+ mCategoryKeyboardMap.put(key, kbd);
+ }
+ } else {
+ kbd = mCategoryKeyboardMap.get(key);
+ }
+ return kbd;
+ }
+ }
+
+ public int getTotalPageCountOfAllCategories() {
+ int sum = 0;
+ for (CategoryProperties properties : mShownCategories) {
+ sum += properties.mPageCount;
+ }
+ 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;
+ }
+ });
+ final int pageCount = (keys.length - 1) / maxPageCount + 1;
+ final Key[][] retval = new Key[pageCount][maxPageCount];
+ for (int i = 0; i < keys.length; ++i) {
+ retval[i / maxPageCount][i % maxPageCount] = keys[i];
+ }
+ return retval;
+ }
+ }
+
+ private final EmojiCategory mEmojiCategory;
+
+ public EmojiKeyboardView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, R.attr.emojiKeyboardViewStyle);
+ }
+
+ public EmojiKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+ final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
+ R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
+ mKeyBackgroundId = keyboardViewAttr.getResourceId(
+ R.styleable.KeyboardView_keyBackground, 0);
+ mEmojiFunctionalKeyBackgroundId = keyboardViewAttr.getResourceId(
+ R.styleable.KeyboardView_keyBackgroundEmojiFunctional, 0);
+ keyboardViewAttr.recycle();
+ final TypedArray emojiKeyboardViewAttr = context.obtainStyledAttributes(attrs,
+ R.styleable.EmojiKeyboardView, defStyle, R.style.EmojiKeyboardView);
+ mTabLabelColor = emojiKeyboardViewAttr.getColorStateList(
+ R.styleable.EmojiKeyboardView_emojiTabLabelColor);
+ emojiKeyboardViewAttr.recycle();
+ final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
+ context, null /* editorInfo */);
+ final Resources res = context.getResources();
+ final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res);
+ builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype());
+ builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res),
+ emojiLp.mEmojiKeyboardHeight);
+ builder.setOptions(false, false, false /* lanuageSwitchKeyEnabled */);
+ mLayoutSet = builder.build();
+ mEmojiCategory = new EmojiCategory(PreferenceManager.getDefaultSharedPreferences(context),
+ context.getResources(), builder.build());
+ mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener(context);
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ final Resources res = getContext().getResources();
+ // The main keyboard expands to the entire this {@link KeyboardView}.
+ final int width = ResourceUtils.getDefaultKeyboardWidth(res)
+ + getPaddingLeft() + getPaddingRight();
+ final int height = ResourceUtils.getDefaultKeyboardHeight(res)
+ + res.getDimensionPixelSize(R.dimen.suggestions_strip_height)
+ + getPaddingTop() + getPaddingBottom();
+ setMeasuredDimension(width, height);
+ }
+
+ private void addTab(final TabHost host, final int categoryId) {
+ final String tabId = mEmojiCategory.getCategoryName(categoryId, 0 /* categoryPageId */);
+ final TabHost.TabSpec tspec = host.newTabSpec(tabId);
+ tspec.setContent(R.id.emoji_keyboard_dummy);
+ if (mEmojiCategory.getCategoryIcon(categoryId) != 0) {
+ final ImageView iconView = (ImageView)LayoutInflater.from(getContext()).inflate(
+ R.layout.emoji_keyboard_tab_icon, null);
+ iconView.setImageResource(mEmojiCategory.getCategoryIcon(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.setTextColor(mTabLabelColor);
+ tspec.setIndicator(textView);
+ }
+ host.addTab(tspec);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ mTabHost = (TabHost)findViewById(R.id.emoji_category_tabhost);
+ mTabHost.setup();
+ for (final CategoryProperties properties : mEmojiCategory.getShownCategories()) {
+ addTab(mTabHost, properties.mCategoryId);
+ }
+ mTabHost.setOnTabChangedListener(this);
+ mTabHost.getTabWidget().setStripEnabled(true);
+
+ mEmojiKeyboardAdapter = new EmojiKeyboardAdapter(mEmojiCategory, mLayoutSet, this);
+
+ mEmojiPager = (ViewPager)findViewById(R.id.emoji_keyboard_pager);
+ mEmojiPager.setAdapter(mEmojiKeyboardAdapter);
+ mEmojiPager.setOnPageChangeListener(this);
+ mEmojiPager.setOffscreenPageLimit(0);
+ final Resources res = getResources();
+ final EmojiLayoutParams emojiLp = new EmojiLayoutParams(res);
+ emojiLp.setPagerProperties(mEmojiPager);
+
+ mEmojiCategoryPageIndicatorView =
+ (EmojiCategoryPageIndicatorView)findViewById(R.id.emoji_category_page_id_view);
+ emojiLp.setCategoryPageIdViewProperties(mEmojiCategoryPageIndicatorView);
+
+ setCurrentCategoryId(mEmojiCategory.getCurrentCategoryId(), true /* force */);
+
+ final LinearLayout actionBar = (LinearLayout)findViewById(R.id.emoji_action_bar);
+ emojiLp.setActionBarProperties(actionBar);
+
+ 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);
+ final ImageView spaceKey = (ImageView)findViewById(R.id.emoji_keyboard_space);
+ spaceKey.setBackgroundResource(mKeyBackgroundId);
+ spaceKey.setTag(Constants.CODE_SPACE);
+ spaceKey.setOnClickListener(this);
+ emojiLp.setKeyProperties(spaceKey);
+ final ImageView sendKey = (ImageView)findViewById(R.id.emoji_keyboard_send);
+ sendKey.setBackgroundResource(mEmojiFunctionalKeyBackgroundId);
+ sendKey.setTag(Constants.CODE_ENTER);
+ sendKey.setOnClickListener(this);
+ }
+
+ @Override
+ public void onTabChanged(final String tabId) {
+ final int categoryId = mEmojiCategory.getCategoryId(tabId);
+ setCurrentCategoryId(categoryId, false /* force */);
+ updateEmojiCategoryPageIdView();
+ }
+
+
+ @Override
+ public void onPageSelected(final int position) {
+ final Pair<Integer, Integer> newPos =
+ mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position);
+ setCurrentCategoryId(newPos.first /* categoryId */, false /* force */);
+ mEmojiCategory.setCurrentCategoryPageId(newPos.second /* categoryPageId */);
+ updateEmojiCategoryPageIdView();
+ }
+
+ @Override
+ public void onPageScrollStateChanged(final int state) {
+ // Ignore this message. Only want the actual page selected.
+ }
+
+ @Override
+ public void onPageScrolled(final int position, final float positionOffset,
+ final int positionOffsetPixels) {
+ final Pair<Integer, Integer> newPos =
+ mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position);
+ final int newCategoryId = newPos.first;
+ final int newCategorySize = mEmojiCategory.getCategoryPageSize(newCategoryId);
+ final int currentCategoryId = mEmojiCategory.getCurrentCategoryId();
+ final int currentCategoryPageId = mEmojiCategory.getCurrentCategoryPageId();
+ final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageSize();
+ if (newCategoryId == currentCategoryId) {
+ mEmojiCategoryPageIndicatorView.setCategoryPageId(
+ newCategorySize, newPos.second, positionOffset);
+ } else if (newCategoryId > currentCategoryId) {
+ mEmojiCategoryPageIndicatorView.setCategoryPageId(
+ currentCategorySize, currentCategoryPageId, positionOffset);
+ } else if (newCategoryId < currentCategoryId) {
+ mEmojiCategoryPageIndicatorView.setCategoryPageId(
+ currentCategorySize, currentCategoryPageId, positionOffset - 1);
+ }
+ }
+
+ @Override
+ public void onClick(final View v) {
+ if (v.getTag() instanceof Integer) {
+ final int code = (Integer)v.getTag();
+ registerCode(code);
+ return;
+ }
+ }
+
+ private void registerCode(final int code) {
+ mKeyboardActionListener.onPressKey(code, 0 /* repeatCount */, true /* isSinglePointer */);
+ mKeyboardActionListener.onCodeInput(code, NOT_A_COORDINATE, NOT_A_COORDINATE);
+ mKeyboardActionListener.onReleaseKey(code, false /* withSliding */);
+ }
+
+ @Override
+ public void onKeyClick(final Key key) {
+ mEmojiKeyboardAdapter.addRecentKey(key);
+ mEmojiCategory.saveLastTypedCategoryPage();
+ final int code = key.getCode();
+ if (code == Constants.CODE_OUTPUT_TEXT) {
+ mKeyboardActionListener.onTextInput(key.getOutputText());
+ return;
+ }
+ registerCode(code);
+ }
+
+ public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
+ // TODO:
+ }
+
+ public void setKeyboardActionListener(final KeyboardActionListener listener) {
+ mKeyboardActionListener = listener;
+ mDeleteKeyOnTouchListener.setKeyboardActionListener(mKeyboardActionListener);
+ }
+
+ private void updateEmojiCategoryPageIdView() {
+ if (mEmojiCategoryPageIndicatorView == null) {
+ return;
+ }
+ mEmojiCategoryPageIndicatorView.setCategoryPageId(
+ mEmojiCategory.getCurrentCategoryPageSize(),
+ mEmojiCategory.getCurrentCategoryPageId(), 0.0f /* offset */);
+ }
+
+ private void setCurrentCategoryId(final int categoryId, final boolean force) {
+ if (mEmojiCategory.getCurrentCategoryId() == categoryId && !force) {
+ return;
+ }
+
+ mEmojiCategory.setCurrentCategoryId(categoryId);
+ final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId);
+ final int newCategoryPageId = mEmojiCategory.getPageIdFromCategoryId(categoryId);
+ if (force || mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(
+ mEmojiPager.getCurrentItem()).first != categoryId) {
+ mEmojiPager.setCurrentItem(newCategoryPageId, false /* smoothScroll */);
+ }
+ if (force || mTabHost.getCurrentTab() != newTabId) {
+ mTabHost.setCurrentTab(newTabId);
+ }
+ }
+
+ private static class EmojiKeyboardAdapter extends PagerAdapter {
+ private final ScrollKeyboardView.OnKeyClickListener mListener;
+ private final DynamicGridKeyboard mRecentsKeyboard;
+ private final SparseArray<ScrollKeyboardView> mActiveKeyboardView =
+ CollectionUtils.newSparseArray();
+ private final EmojiCategory mEmojiCategory;
+ private int mActivePosition = 0;
+
+ public EmojiKeyboardAdapter(final EmojiCategory emojiCategory,
+ final KeyboardLayoutSet layoutSet,
+ final ScrollKeyboardView.OnKeyClickListener listener) {
+ mEmojiCategory = emojiCategory;
+ mListener = listener;
+ mRecentsKeyboard = mEmojiCategory.getKeyboard(CATEGORY_ID_RECENTS, 0);
+ }
+
+ public void addRecentKey(final Key key) {
+ if (mEmojiCategory.isInRecentTab()) {
+ return;
+ }
+ mRecentsKeyboard.addKeyFirst(key);
+ final KeyboardView recentKeyboardView =
+ mActiveKeyboardView.get(mEmojiCategory.getRecentTabId());
+ if (recentKeyboardView != null) {
+ recentKeyboardView.invalidateAllKeys();
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return mEmojiCategory.getTotalPageCountOfAllCategories();
+ }
+
+ @Override
+ public void setPrimaryItem(final View container, final int position, final Object object) {
+ if (mActivePosition == position) {
+ return;
+ }
+ final ScrollKeyboardView oldKeyboardView = mActiveKeyboardView.get(mActivePosition);
+ if (oldKeyboardView != null) {
+ oldKeyboardView.releaseCurrentKey();
+ oldKeyboardView.deallocateMemory();
+ }
+ mActivePosition = position;
+ }
+
+ @Override
+ public Object instantiateItem(final ViewGroup container, final int position) {
+ final Keyboard keyboard =
+ mEmojiCategory.getKeyboardFromPagePosition(position);
+ final LayoutInflater inflater = LayoutInflater.from(container.getContext());
+ final View view = 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);
+ mActiveKeyboardView.put(position, keyboardView);
+ return view;
+ }
+
+ @Override
+ public boolean isViewFromObject(final View view, final Object object) {
+ return view == object;
+ }
+
+ @Override
+ public void destroyItem(final ViewGroup container, final int position,
+ final Object object) {
+ final ScrollKeyboardView keyboardView = mActiveKeyboardView.get(position);
+ if (keyboardView != null) {
+ keyboardView.deallocateMemory();
+ mActiveKeyboardView.remove(position);
+ }
+ container.removeView(keyboardView);
+ }
+ }
+
+ // 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 final int mDeleteKeyPressedBackgroundColor;
+ private final long mKeyRepeatStartTimeout;
+ private final long mKeyRepeatInterval;
+
+ public DeleteKeyOnTouchListener(Context context) {
+ final Resources res = context.getResources();
+ mDeleteKeyPressedBackgroundColor =
+ 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);
+ }
+
+ private KeyboardActionListener mKeyboardActionListener =
+ KeyboardActionListener.EMPTY_LISTENER;
+ private DummyRepeatKeyRepeatTimer mTimer;
+
+ private synchronized void startRepeat() {
+ if (mTimer != null) {
+ abortRepeat();
+ }
+ mTimer = new DummyRepeatKeyRepeatTimer();
+ mTimer.start();
+ }
+
+ private synchronized void abortRepeat() {
+ mTimer.abort();
+ mTimer = null;
+ }
+
+ // 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 timeCount = 0;
+ while (timeCount < MAX_REPEAT_COUNT_TIME && !mAborted) {
+ if (timeCount > mKeyRepeatStartTimeout) {
+ pressDelete();
+ }
+ timeCount += mKeyRepeatInterval;
+ try {
+ Thread.sleep(mKeyRepeatInterval);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ public void abort() {
+ mAborted = true;
+ }
+ }
+
+ public void pressDelete() {
+ mKeyboardActionListener.onPressKey(
+ Constants.CODE_DELETE, 0 /* repeatCount */, true /* isSinglePointer */);
+ mKeyboardActionListener.onCodeInput(
+ Constants.CODE_DELETE, NOT_A_COORDINATE, NOT_A_COORDINATE);
+ mKeyboardActionListener.onReleaseKey(
+ Constants.CODE_DELETE, false /* withSliding */);
+ }
+
+ public void setKeyboardActionListener(KeyboardActionListener listener) {
+ mKeyboardActionListener = listener;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ v.setBackgroundColor(mDeleteKeyPressedBackgroundColor);
+ pressDelete();
+ startRepeat();
+ return true;
+ case MotionEvent.ACTION_UP:
+ v.setBackgroundColor(0);
+ abortRepeat();
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java b/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java
new file mode 100644
index 000000000..267fad5cd
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/EmojiLayoutParams.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard;
+
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.ResourceUtils;
+
+import android.content.res.Resources;
+import android.support.v4.view.ViewPager;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+public class EmojiLayoutParams {
+ private static final int DEFAULT_KEYBOARD_ROWS = 4;
+
+ public final int mEmojiPagerHeight;
+ private final int mEmojiPagerBottomMargin;
+ public final int mEmojiKeyboardHeight;
+ private final int mEmojiCategoryPageIdViewHeight;
+ public final int mEmojiActionBarHeight;
+ public final int mKeyVerticalGap;
+ private final int mKeyHorizontalGap;
+ private final int mBottomPadding;
+ private final int mTopPadding;
+
+ public EmojiLayoutParams(Resources res) {
+ final int defaultKeyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res);
+ final int defaultKeyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res);
+ mKeyVerticalGap = (int) res.getFraction(R.fraction.key_bottom_gap_ics,
+ (int) defaultKeyboardHeight, (int) defaultKeyboardHeight);
+ mBottomPadding = (int) res.getFraction(R.fraction.keyboard_bottom_padding_ics,
+ (int) defaultKeyboardHeight, (int) defaultKeyboardHeight);
+ mTopPadding = (int) res.getFraction(R.fraction.keyboard_top_padding_ics,
+ (int) defaultKeyboardHeight, (int) defaultKeyboardHeight);
+ mKeyHorizontalGap = (int) (res.getFraction(R.fraction.key_horizontal_gap_ics,
+ defaultKeyboardWidth, defaultKeyboardWidth));
+ mEmojiCategoryPageIdViewHeight =
+ (int) (res.getDimension(R.dimen.emoji_category_page_id_height));
+ final int baseheight = defaultKeyboardHeight - mBottomPadding - mTopPadding
+ + mKeyVerticalGap;
+ mEmojiActionBarHeight = ((int) baseheight) / DEFAULT_KEYBOARD_ROWS
+ - (mKeyVerticalGap - mBottomPadding) / 2;
+ mEmojiPagerHeight = defaultKeyboardHeight - mEmojiActionBarHeight
+ - mEmojiCategoryPageIdViewHeight;
+ mEmojiPagerBottomMargin = mKeyVerticalGap / 2;
+ mEmojiKeyboardHeight = mEmojiPagerHeight - mEmojiPagerBottomMargin - 1;
+ }
+
+ public void setPagerProperties(ViewPager vp) {
+ final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) vp.getLayoutParams();
+ lp.height = mEmojiKeyboardHeight;
+ lp.bottomMargin = mEmojiPagerBottomMargin;
+ vp.setLayoutParams(lp);
+ }
+
+ public void setCategoryPageIdViewProperties(LinearLayout ll) {
+ final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams();
+ lp.height = mEmojiCategoryPageIdViewHeight;
+ ll.setLayoutParams(lp);
+ }
+
+ public void setActionBarProperties(LinearLayout ll) {
+ final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams();
+ lp.height = mEmojiActionBarHeight;
+ lp.topMargin = 0;
+ lp.bottomMargin = mBottomPadding;
+ ll.setLayoutParams(lp);
+ }
+
+ public void setKeyProperties(ImageView ib) {
+ final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ib.getLayoutParams();
+ lp.leftMargin = mKeyHorizontalGap / 2;
+ lp.rightMargin = mKeyHorizontalGap / 2;
+ ib.setLayoutParams(lp);
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 4ef8653f6..3ea68806b 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -41,7 +41,7 @@ import com.android.inputmethod.keyboard.internal.KeyboardRow;
import com.android.inputmethod.keyboard.internal.MoreKeySpec;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.StringUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -58,12 +58,12 @@ public class Key implements Comparable<Key> {
/**
* The key code (unicode or custom code) that this key generates.
*/
- public final int mCode;
+ private final int mCode;
/** Label to display */
- public final String mLabel;
+ private final String mLabel;
/** Hint label to display on the key in conjunction with the label */
- public final String mHintLabel;
+ private final String mHintLabel;
/** Flags of the label */
private final int mLabelFlags;
private static final int LABEL_FLAGS_ALIGN_LEFT = 0x01;
@@ -95,18 +95,18 @@ public class Key implements Comparable<Key> {
private final int mIconId;
/** Width of the key, not including the gap */
- public final int mWidth;
+ private final int mWidth;
/** Height of the key, not including the gap */
- public final int mHeight;
+ private final int mHeight;
/** X coordinate of the key in the keyboard layout */
- public final int mX;
+ private final int mX;
/** Y coordinate of the key in the keyboard layout */
- public final int mY;
+ private final int mY;
/** Hit bounding box of the key */
- public final Rect mHitBox = new Rect();
+ private final Rect mHitBox = new Rect();
/** More keys. It is guaranteed that this is null or an array of one or more elements */
- public final MoreKeySpec[] mMoreKeys;
+ private final MoreKeySpec[] mMoreKeys;
/** More keys column number and flags */
private final int mMoreKeysColumnAndFlags;
private static final int MORE_KEYS_COLUMN_MASK = 0x000000ff;
@@ -121,12 +121,13 @@ public class Key implements Comparable<Key> {
private static final String MORE_KEYS_NO_PANEL_AUTO_MORE_KEY = "!noPanelAutoMoreKey!";
/** Background type that represents different key background visual than normal one. */
- public final int mBackgroundType;
- public static final int BACKGROUND_TYPE_NORMAL = 0;
- public static final int BACKGROUND_TYPE_FUNCTIONAL = 1;
- public static final int BACKGROUND_TYPE_ACTION = 2;
- public static final int BACKGROUND_TYPE_STICKY_OFF = 3;
- public static final int BACKGROUND_TYPE_STICKY_ON = 4;
+ private final int mBackgroundType;
+ public static final int BACKGROUND_TYPE_EMPTY = 0;
+ public static final int BACKGROUND_TYPE_NORMAL = 1;
+ public static final int BACKGROUND_TYPE_FUNCTIONAL = 2;
+ public static final int BACKGROUND_TYPE_ACTION = 3;
+ public static final int BACKGROUND_TYPE_STICKY_OFF = 4;
+ public static final int BACKGROUND_TYPE_STICKY_ON = 5;
private final int mActionFlags;
private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01;
@@ -134,7 +135,7 @@ public class Key implements Comparable<Key> {
private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04;
private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08;
- public final KeyVisualAttributes mKeyVisualAttributes;
+ private final KeyVisualAttributes mKeyVisualAttributes;
private final OptionalAttributes mOptionalAttributes;
@@ -150,7 +151,7 @@ public class Key implements Comparable<Key> {
public final int mVisualInsetsLeft;
public final int mVisualInsetsRight;
- public OptionalAttributes(final String outputText, final int altCode,
+ private OptionalAttributes(final String outputText, final int altCode,
final int disabledIconId, final int previewIconId,
final int visualInsetsLeft, final int visualInsetsRight) {
mOutputText = outputText;
@@ -160,6 +161,18 @@ public class Key implements Comparable<Key> {
mVisualInsetsLeft = visualInsetsLeft;
mVisualInsetsRight = visualInsetsRight;
}
+
+ public static OptionalAttributes newInstance(final String outputText, final int altCode,
+ final int disabledIconId, final int previewIconId,
+ final int visualInsetsLeft, final int visualInsetsRight) {
+ if (outputText == null && altCode == CODE_UNSPECIFIED
+ && disabledIconId == ICON_UNDEFINED && previewIconId == ICON_UNDEFINED
+ && visualInsetsLeft == 0 && visualInsetsRight == 0) {
+ return null;
+ }
+ return new OptionalAttributes(outputText, altCode, disabledIconId, previewIconId,
+ visualInsetsLeft, visualInsetsRight);
+ }
}
private final int mHashCode;
@@ -175,7 +188,7 @@ public class Key implements Comparable<Key> {
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);
+ moreKeySpec.mOutputText, x, y, width, height, labelFlags, BACKGROUND_TYPE_NORMAL);
}
/**
@@ -183,22 +196,19 @@ public class Key implements Comparable<Key> {
*/
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 width, final int height, final int labelFlags, final int backgroundType) {
mHeight = height - params.mVerticalGap;
mWidth = width - params.mHorizontalGap;
mHintLabel = hintLabel;
mLabelFlags = labelFlags;
- mBackgroundType = BACKGROUND_TYPE_NORMAL;
+ mBackgroundType = backgroundType;
mActionFlags = 0;
mMoreKeys = null;
mMoreKeysColumnAndFlags = 0;
mLabel = label;
- if (outputText == null) {
- mOptionalAttributes = null;
- } else {
- mOptionalAttributes = new OptionalAttributes(outputText, CODE_UNSPECIFIED,
- ICON_UNDEFINED, ICON_UNDEFINED, 0, 0);
- }
+ mOptionalAttributes = OptionalAttributes.newInstance(outputText, CODE_UNSPECIFIED,
+ ICON_UNDEFINED, ICON_UNDEFINED,
+ 0 /* visualInsetsLeft */, 0 /* visualInsetsRight */);
mCode = code;
mEnabled = (code != CODE_UNSPECIFIED);
mIconId = iconId;
@@ -224,7 +234,7 @@ public class Key implements Comparable<Key> {
public Key(final Resources res, final KeyboardParams params, final KeyboardRow row,
final XmlPullParser parser) throws XmlPullParserException {
final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
- final int rowHeight = row.mRowHeight;
+ final int rowHeight = row.getRowHeight();
mHeight = rowHeight - params.mVerticalGap;
final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
@@ -259,11 +269,11 @@ public class Key implements Comparable<Key> {
final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr,
R.styleable.Keyboard_Key_keyIconPreview));
- mLabelFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
+ mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
| row.getDefaultKeyLabelFlags();
final boolean needsToUpperCase = needsToUpperCase(mLabelFlags, params.mId.mElementId);
final Locale locale = params.mId.mLocale;
- int actionFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
+ int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
int moreKeysColumn = style.getInt(keyAttr,
@@ -306,8 +316,15 @@ public class Key implements Comparable<Key> {
}
mActionFlags = actionFlags;
+ final int code = KeySpecParser.parseCode(style.getString(keyAttr,
+ R.styleable.Keyboard_Key_code), params.mCodesSet, CODE_UNSPECIFIED);
if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) {
mLabel = params.mId.mCustomActionLabel;
+ } else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
+ // This is a workaround to have a key that has a supplementary code point in its label.
+ // Because we can put a string in resource neither as a XML entity of a supplementary
+ // 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);
@@ -320,8 +337,6 @@ public class Key implements Comparable<Key> {
}
String outputText = KeySpecParser.toUpperCaseOfStringForLocale(style.getString(keyAttr,
R.styleable.Keyboard_Key_keyOutputText), needsToUpperCase, locale);
- final int code = KeySpecParser.parseCode(style.getString(keyAttr,
- R.styleable.Keyboard_Key_code), params.mCodesSet, CODE_UNSPECIFIED);
// Choose the first letter of the label as primary code if not specified.
if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText)
&& !TextUtils.isEmpty(mLabel)) {
@@ -354,15 +369,8 @@ public class Key implements Comparable<Key> {
KeySpecParser.parseCode(style.getString(keyAttr,
R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED),
needsToUpperCase, locale);
- if (outputText == null && altCode == CODE_UNSPECIFIED
- && disabledIconId == ICON_UNDEFINED && previewIconId == ICON_UNDEFINED
- && visualInsetsLeft == 0 && visualInsetsRight == 0) {
- mOptionalAttributes = null;
- } else {
- mOptionalAttributes = new OptionalAttributes(outputText, altCode,
- disabledIconId, previewIconId,
- visualInsetsLeft, visualInsetsRight);
- }
+ mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode,
+ disabledIconId, previewIconId, visualInsetsLeft, visualInsetsRight);
mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
keyAttr.recycle();
mHashCode = computeHashCode(this);
@@ -371,6 +379,35 @@ public class Key implements Comparable<Key> {
}
}
+ /**
+ * Copy constructor.
+ *
+ * @param key the original key.
+ */
+ protected Key(final Key key) {
+ // Final attributes.
+ mCode = key.mCode;
+ mLabel = key.mLabel;
+ mHintLabel = key.mHintLabel;
+ mLabelFlags = key.mLabelFlags;
+ mIconId = key.mIconId;
+ mWidth = key.mWidth;
+ mHeight = key.mHeight;
+ mX = key.mX;
+ mY = key.mY;
+ mHitBox.set(key.mHitBox);
+ mMoreKeys = key.mMoreKeys;
+ mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags;
+ mBackgroundType = key.mBackgroundType;
+ mActionFlags = key.mActionFlags;
+ mKeyVisualAttributes = key.mKeyVisualAttributes;
+ mOptionalAttributes = key.mOptionalAttributes;
+ mHashCode = key.mHashCode;
+ // Key state.
+ mPressed = key.mPressed;
+ mEnabled = key.mEnabled;
+ }
+
private static boolean needsToUpperCase(final int labelFlags, final int keyboardElementId) {
if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
switch (keyboardElementId) {
@@ -460,6 +497,7 @@ public class Key implements Comparable<Key> {
private static String backgroundName(final int backgroundType) {
switch (backgroundType) {
+ case BACKGROUND_TYPE_EMPTY: return "empty";
case BACKGROUND_TYPE_NORMAL: return "normal";
case BACKGROUND_TYPE_FUNCTIONAL: return "functional";
case BACKGROUND_TYPE_ACTION: return "action";
@@ -469,6 +507,22 @@ public class Key implements Comparable<Key> {
}
}
+ public int getCode() {
+ return mCode;
+ }
+
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public String getHintLabel() {
+ return mHintLabel;
+ }
+
+ public MoreKeySpec[] getMoreKeys() {
+ return mMoreKeys;
+ }
+
public void markAsLeftEdge(final KeyboardParams params) {
mHitBox.left = params.mLeftPadding;
}
@@ -515,6 +569,10 @@ public class Key implements Comparable<Key> {
&& (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0;
}
+ public KeyVisualAttributes getVisualAttributes() {
+ return mKeyVisualAttributes;
+ }
+
public final Typeface selectTypeface(final KeyDrawParams params) {
// TODO: Handle "bold" here too?
if ((mLabelFlags & LABEL_FLAGS_FONT_NORMAL) != 0) {
@@ -689,9 +747,26 @@ public class Key implements Comparable<Key> {
? iconSet.getIconDrawable(previewIconId) : iconSet.getIconDrawable(mIconId);
}
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public int getX() {
+ return mX;
+ }
+
+ public int getY() {
+ return mY;
+ }
+
public final int getDrawX() {
+ final int x = getX();
final OptionalAttributes attrs = mOptionalAttributes;
- return (attrs == null) ? mX : mX + attrs.mVisualInsetsLeft;
+ return (attrs == null) ? x : x + attrs.mVisualInsetsLeft;
}
public final int getDrawWidth() {
@@ -726,6 +801,10 @@ public class Key implements Comparable<Key> {
mEnabled = enabled;
}
+ public Rect getHitBox() {
+ return mHitBox;
+ }
+
/**
* Detects if a point falls on this key.
* @param x the x-coordinate of the point
@@ -745,9 +824,9 @@ public class Key implements Comparable<Key> {
* @return the square of the distance of the point from the nearest edge of the key
*/
public int squaredDistanceToEdge(final int x, final int y) {
- final int left = mX;
+ final int left = getX();
final int right = left + mWidth;
- final int top = mY;
+ final int top = getY();
final int bottom = top + mHeight;
final int edgeX = x < left ? left : (x > right ? right : x);
final int edgeY = y < top ? top : (y > bottom ? bottom : y);
@@ -783,6 +862,10 @@ public class Key implements Comparable<Key> {
android.R.attr.state_pressed
};
+ private final static int[] KEY_STATE_EMPTY = {
+ android.R.attr.state_empty
+ };
+
// functional normal state (with properties)
private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
android.R.attr.state_single
@@ -820,6 +903,8 @@ public class Key implements Comparable<Key> {
return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_OFF : KEY_STATE_NORMAL_HIGHLIGHT_OFF;
case BACKGROUND_TYPE_STICKY_ON:
return mPressed ? KEY_STATE_PRESSED_HIGHLIGHT_ON : KEY_STATE_NORMAL_HIGHLIGHT_ON;
+ case BACKGROUND_TYPE_EMPTY:
+ return mPressed ? KEY_STATE_PRESSED : KEY_STATE_EMPTY;
default: /* BACKGROUND_TYPE_NORMAL */
return mPressed ? KEY_STATE_PRESSED : KEY_STATE_NORMAL;
}
@@ -837,7 +922,7 @@ 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);
+ null, x, y, width, height, 0, BACKGROUND_TYPE_EMPTY);
}
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
index 17e707f95..befb6fa92 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
@@ -108,8 +108,9 @@ public class KeyDetector {
if (distance > minDistance) {
continue;
}
- // To take care of hitbox overlaps, we compare mCode here too.
- if (primaryKey == null || distance < minDistance || key.mCode > primaryKey.mCode) {
+ // To take care of hitbox overlaps, we compare key's code here too.
+ if (primaryKey == null || distance < minDistance
+ || key.getCode() > primaryKey.getCode()) {
minDistance = distance;
primaryKey = key;
}
@@ -118,7 +119,7 @@ public class KeyDetector {
}
public static String printableCode(Key key) {
- return key != null ? Constants.printableCode(key.mCode) : "none";
+ return key != null ? Constants.printableCode(key.getCode()) : "none";
}
public static String printableCodes(int[] codes) {
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index e87ecbc7e..bc1383aff 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -21,8 +21,8 @@ import android.util.SparseArray;
import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
import com.android.inputmethod.keyboard.internal.KeyboardParams;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.utils.CollectionUtils;
/**
* Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
@@ -51,6 +51,11 @@ public class Keyboard {
/** Total width of the keyboard, including the padding and keys */
public final int mOccupiedWidth;
+ /** Base height of the keyboard, used to calculate rows' height */
+ public final int mBaseHeight;
+ /** Base width of the keyboard, used to calculate keys' width */
+ public final int mBaseWidth;
+
/** The padding above the keyboard */
public final int mTopPadding;
/** Default gap between rows */
@@ -69,7 +74,7 @@ public class Keyboard {
public final int mMaxMoreKeysKeyboardColumn;
/** Array of keys and icons in this keyboard */
- public final Key[] mKeys;
+ private final Key[] mKeys;
public final Key[] mShiftKeys;
public final Key[] mAltCodeKeysWhileTyping;
public final KeyboardIconsSet mIconsSet;
@@ -84,6 +89,8 @@ public class Keyboard {
mThemeId = params.mThemeId;
mOccupiedHeight = params.mOccupiedHeight;
mOccupiedWidth = params.mOccupiedWidth;
+ mBaseHeight = params.mBaseHeight;
+ mBaseWidth = params.mBaseWidth;
mMostCommonKeyHeight = params.mMostCommonKeyHeight;
mMostCommonKeyWidth = params.mMostCommonKeyWidth;
mMoreKeysTemplate = params.mMoreKeysTemplate;
@@ -104,6 +111,30 @@ public class Keyboard {
mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled;
}
+ protected Keyboard(final Keyboard keyboard) {
+ mId = keyboard.mId;
+ mThemeId = keyboard.mThemeId;
+ mOccupiedHeight = keyboard.mOccupiedHeight;
+ mOccupiedWidth = keyboard.mOccupiedWidth;
+ mBaseHeight = keyboard.mBaseHeight;
+ mBaseWidth = keyboard.mBaseWidth;
+ mMostCommonKeyHeight = keyboard.mMostCommonKeyHeight;
+ mMostCommonKeyWidth = keyboard.mMostCommonKeyWidth;
+ mMoreKeysTemplate = keyboard.mMoreKeysTemplate;
+ mMaxMoreKeysKeyboardColumn = keyboard.mMaxMoreKeysKeyboardColumn;
+ mKeyVisualAttributes = keyboard.mKeyVisualAttributes;
+ mTopPadding = keyboard.mTopPadding;
+ mVerticalGap = keyboard.mVerticalGap;
+
+ mKeys = keyboard.mKeys;
+ mShiftKeys = keyboard.mShiftKeys;
+ mAltCodeKeysWhileTyping = keyboard.mAltCodeKeysWhileTyping;
+ mIconsSet = keyboard.mIconsSet;
+
+ mProximityInfo = keyboard.mProximityInfo;
+ mProximityCharsCorrectionEnabled = keyboard.mProximityCharsCorrectionEnabled;
+ }
+
public boolean hasProximityCharsCorrection(final int code) {
if (!mProximityCharsCorrectionEnabled) {
return false;
@@ -120,6 +151,19 @@ public class Keyboard {
return mProximityInfo;
}
+ public Key[] getKeys() {
+ return mKeys;
+ }
+
+ public Key getKeyFromOutputText(final String outputText) {
+ for (final Key key : getKeys()) {
+ if (outputText.equals(key.getOutputText())) {
+ return key;
+ }
+ }
+ return null;
+ }
+
public Key getKey(final int code) {
if (code == Constants.CODE_UNSPECIFIED) {
return null;
@@ -130,8 +174,8 @@ public class Keyboard {
return mKeyCache.valueAt(index);
}
- for (final Key key : mKeys) {
- if (key.mCode == code) {
+ for (final Key key : getKeys()) {
+ if (key.getCode() == code) {
mKeyCache.put(code, key);
return key;
}
@@ -146,9 +190,9 @@ public class Keyboard {
return true;
}
- for (final Key key : mKeys) {
+ for (final Key key : getKeys()) {
if (key == aKey) {
- mKeyCache.put(key.mCode, key);
+ mKeyCache.put(key.getCode(), key);
return true;
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
index 9eeee5baf..dc760e685 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
@@ -20,16 +20,16 @@ import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.InputPointers;
public interface KeyboardActionListener {
-
/**
* Called when the user presses a key. This is sent before the {@link #onCodeInput} is called.
* For keys that repeat, this is only called once.
*
* @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key,
* the value will be zero.
+ * @param repeatCount how many times the key was repeated. Zero if it is the first press.
* @param isSinglePointer true if pressing has occurred while no other key is being pressed.
*/
- public void onPressKey(int primaryCode, boolean isSinglePointer);
+ public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer);
/**
* Called when the user releases a key. This is sent after the {@link #onCodeInput} is called.
@@ -99,11 +99,11 @@ public interface KeyboardActionListener {
*/
public boolean onCustomRequest(int requestCode);
- public static class Adapter implements KeyboardActionListener {
- public static final Adapter EMPTY_LISTENER = new Adapter();
+ public static final KeyboardActionListener EMPTY_LISTENER = new Adapter();
+ public static class Adapter implements KeyboardActionListener {
@Override
- public void onPressKey(int primaryCode, boolean isSinglePointer) {}
+ public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer) {}
@Override
public void onReleaseKey(int primaryCode, boolean withSliding) {}
@Override
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index 4c5dd25c4..736f13ed6 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -18,15 +18,14 @@ package com.android.inputmethod.keyboard;
import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
-import android.content.res.Configuration;
import android.text.InputType;
import android.text.TextUtils;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.compat.EditorInfoCompatUtils;
-import com.android.inputmethod.latin.InputTypeUtils;
-import com.android.inputmethod.latin.SubtypeLocale;
+import com.android.inputmethod.latin.utils.InputTypeUtils;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
import java.util.Arrays;
import java.util.Locale;
@@ -55,11 +54,16 @@ public final class KeyboardId {
public static final int ELEMENT_PHONE = 7;
public static final int ELEMENT_PHONE_SYMBOLS = 8;
public static final int ELEMENT_NUMBER = 9;
+ public static final int ELEMENT_EMOJI_RECENTS = 10;
+ public static final int ELEMENT_EMOJI_CATEGORY1 = 11;
+ public static final int ELEMENT_EMOJI_CATEGORY2 = 12;
+ public static final int ELEMENT_EMOJI_CATEGORY3 = 13;
+ public static final int ELEMENT_EMOJI_CATEGORY4 = 14;
+ public static final int ELEMENT_EMOJI_CATEGORY5 = 15;
+ public static final int ELEMENT_EMOJI_CATEGORY6 = 16;
public final InputMethodSubtype mSubtype;
public final Locale mLocale;
- // TODO: Remove this member. It is used only for logging purpose.
- public final int mOrientation;
public final int mWidth;
public final int mHeight;
public final int mMode;
@@ -76,8 +80,7 @@ public final class KeyboardId {
public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params) {
mSubtype = params.mSubtype;
- mLocale = SubtypeLocale.getSubtypeLocale(mSubtype);
- mOrientation = params.mOrientation;
+ mLocale = SubtypeLocaleUtils.getSubtypeLocale(mSubtype);
mWidth = params.mKeyboardWidth;
mHeight = params.mKeyboardHeight;
mMode = params.mMode;
@@ -101,7 +104,6 @@ public final class KeyboardId {
private static int computeHashCode(final KeyboardId id) {
return Arrays.hashCode(new Object[] {
- id.mOrientation,
id.mElementId,
id.mMode,
id.mWidth,
@@ -123,8 +125,7 @@ public final class KeyboardId {
private boolean equals(final KeyboardId other) {
if (other == this)
return true;
- return other.mOrientation == mOrientation
- && other.mElementId == mElementId
+ return other.mElementId == mElementId
&& other.mMode == mMode
&& other.mWidth == mWidth
&& other.mHeight == mHeight
@@ -185,13 +186,10 @@ public final class KeyboardId {
@Override
public String toString() {
- final String orientation = (mOrientation == Configuration.ORIENTATION_PORTRAIT)
- ? "port" : "land";
- return String.format(Locale.ROOT, "[%s %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%s]",
elementIdToName(mElementId),
- mLocale,
- mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
- orientation, mWidth, mHeight,
+ mLocale, mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
+ mWidth, mHeight,
modeName(mMode),
imeAction(),
(navigateNext() ? "navigateNext" : ""),
@@ -226,6 +224,13 @@ public final class KeyboardId {
case ELEMENT_PHONE: return "phone";
case ELEMENT_PHONE_SYMBOLS: return "phoneSymbols";
case ELEMENT_NUMBER: return "number";
+ case ELEMENT_EMOJI_RECENTS: return "emojiRecents";
+ case ELEMENT_EMOJI_CATEGORY1: return "emojiCategory1";
+ case ELEMENT_EMOJI_CATEGORY2: return "emojiCategory2";
+ case ELEMENT_EMOJI_CATEGORY3: return "emojiCategory3";
+ case ELEMENT_EMOJI_CATEGORY4: return "emojiCategory4";
+ case ELEMENT_EMOJI_CATEGORY5: return "emojiCategory5";
+ case ELEMENT_EMOJI_CATEGORY6: return "emojiCategory6";
default: return null;
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index d4051f74b..1eccdf341 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -23,34 +23,28 @@ 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.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.text.InputType;
-import android.text.TextUtils;
-import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.util.Xml;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodSubtype;
-import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.compat.EditorInfoCompatUtils;
import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
import com.android.inputmethod.keyboard.internal.KeyboardParams;
import com.android.inputmethod.keyboard.internal.KeysCache;
-import com.android.inputmethod.latin.AdditionalSubtype;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.InputAttributes;
-import com.android.inputmethod.latin.InputTypeUtils;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResourceUtils;
-import com.android.inputmethod.latin.SubtypeLocale;
import com.android.inputmethod.latin.SubtypeSwitcher;
-import com.android.inputmethod.latin.XmlParseUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.InputTypeUtils;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
+import com.android.inputmethod.latin.utils.XmlParseUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -74,12 +68,18 @@ public final class KeyboardLayoutSet {
private static final String TAG_ELEMENT = "Element";
private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
- private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480;
- private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 800;
private final Context mContext;
private final Params mParams;
+ // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
+ // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
+ // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
+ private static final int FORCIBLE_CACHE_SIZE = 4;
+ // By construction of soft references, anything that is also referenced somewhere else
+ // will stay in the cache. So we forcibly keep some references in an array to prevent
+ // them from disappearing from sKeyboardCache.
+ private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
CollectionUtils.newHashMap();
private static final KeysCache sKeysCache = new KeysCache();
@@ -106,11 +106,13 @@ public final class KeyboardLayoutSet {
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 mNoSettingsKey;
boolean mLanguageSwitchKeyEnabled;
InputMethodSubtype mSubtype;
- int mOrientation;
+ boolean mIsSpellChecker;
int mKeyboardWidth;
int mKeyboardHeight;
// Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
@@ -162,38 +164,51 @@ public final class KeyboardLayoutSet {
final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
try {
return getKeyboard(elementParams, id);
- } catch (RuntimeException e) {
+ } catch (final RuntimeException e) {
+ Log.e(TAG, "Can't create keyboard: " + id, e);
throw new KeyboardLayoutSetException(e, id);
}
}
private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
- Keyboard keyboard = (ref == null) ? null : ref.get();
- if (keyboard == null) {
- final KeyboardBuilder<KeyboardParams> builder =
- new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams());
- if (id.isAlphabetKeyboard()) {
- builder.setAutoGenerate(sKeysCache);
- }
- final int keyboardXmlId = elementParams.mKeyboardXmlId;
- builder.load(keyboardXmlId, id);
- if (mParams.mDisableTouchPositionCorrectionDataForTest) {
- builder.disableTouchPositionCorrectionDataForTest();
+ final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
+ if (cachedKeyboard != null) {
+ if (DEBUG_CACHE) {
+ Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id);
}
- builder.setProximityCharsCorrectionEnabled(
- elementParams.mProximityCharsCorrectionEnabled);
- keyboard = builder.build();
- sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard));
+ return cachedKeyboard;
+ }
+ final KeyboardBuilder<KeyboardParams> builder =
+ new KeyboardBuilder<KeyboardParams>(mContext, new KeyboardParams());
+ if (id.isAlphabetKeyboard()) {
+ builder.setAutoGenerate(sKeysCache);
+ }
+ final int keyboardXmlId = elementParams.mKeyboardXmlId;
+ builder.load(keyboardXmlId, id);
+ if (mParams.mDisableTouchPositionCorrectionDataForTest) {
+ builder.disableTouchPositionCorrectionDataForTest();
+ }
+ builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled);
+ final Keyboard keyboard = builder.build();
+ sKeyboardCache.put(id, new SoftReference<Keyboard>(keyboard));
+ if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
+ || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
+ && !mParams.mIsSpellChecker) {
+ // We only forcibly cache the primary, "ALPHABET", layouts.
+ for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
+ sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
+ }
+ sForcibleKeyboardCache[0] = keyboard;
if (DEBUG_CACHE) {
- Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
- + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
+ Log.d(TAG, "forcing caching of keyboard with id=" + id);
}
- } else if (DEBUG_CACHE) {
- Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id);
}
-
+ if (DEBUG_CACHE) {
+ Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
+ + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
+ }
return keyboard;
}
@@ -201,7 +216,6 @@ public final class KeyboardLayoutSet {
private final Context mContext;
private final String mPackageName;
private final Resources mResources;
- private final EditorInfo mEditorInfo;
private final Params mParams = new Params();
@@ -211,55 +225,25 @@ public final class KeyboardLayoutSet {
mContext = context;
mPackageName = context.getPackageName();
mResources = context.getResources();
- mEditorInfo = editorInfo;
final Params params = mParams;
params.mMode = getKeyboardMode(editorInfo);
params.mEditorInfo = (editorInfo != null) ? editorInfo : EMPTY_EDITOR_INFO;
params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
- mPackageName, NO_SETTINGS_KEY, mEditorInfo);
+ mPackageName, NO_SETTINGS_KEY, params.mEditorInfo);
}
- public Builder setScreenGeometry(final int widthPixels, final int heightPixels) {
- final Params params = mParams;
- params.mOrientation = (heightPixels > widthPixels)
- ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
- setDefaultKeyboardSize(widthPixels, heightPixels);
+ public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
+ mParams.mKeyboardWidth = keyboardWidth;
+ mParams.mKeyboardHeight = keyboardHeight;
return this;
}
- private void setDefaultKeyboardSize(final int widthPixels, final int heightPixels) {
- final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue(
- mResources, R.array.keyboard_heights);
- final float keyboardHeight;
- if (TextUtils.isEmpty(keyboardHeightString)) {
- keyboardHeight = mResources.getDimension(R.dimen.keyboardHeight);
- } else {
- keyboardHeight = Float.parseFloat(keyboardHeightString)
- * mResources.getDisplayMetrics().density;
- }
- final float maxKeyboardHeight = mResources.getFraction(
- R.fraction.maxKeyboardHeight, heightPixels, heightPixels);
- float minKeyboardHeight = mResources.getFraction(
- R.fraction.minKeyboardHeight, heightPixels, heightPixels);
- if (minKeyboardHeight < 0.0f) {
- // Specified fraction was negative, so it should be calculated against display
- // width.
- minKeyboardHeight = -mResources.getFraction(
- R.fraction.minKeyboardHeight, widthPixels, widthPixels);
- }
- // Keyboard height will not exceed maxKeyboardHeight and will not be less than
- // minKeyboardHeight.
- mParams.mKeyboardHeight = (int)Math.max(
- Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
- mParams.mKeyboardWidth = widthPixels;
- }
-
public Builder setSubtype(final InputMethodSubtype subtype) {
final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
@SuppressWarnings("deprecation")
final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
- mPackageName, FORCE_ASCII, mEditorInfo);
+ mPackageName, FORCE_ASCII, mParams.mEditorInfo);
final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
mParams.mEditorInfo.imeOptions)
|| deprecatedForceAscii;
@@ -268,17 +252,24 @@ public final class KeyboardLayoutSet {
: subtype;
mParams.mSubtype = keyboardSubtype;
mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
- + SubtypeLocale.getKeyboardLayoutSetName(keyboardSubtype);
+ + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype);
+ return this;
+ }
+
+ public Builder setIsSpellChecker(final boolean isSpellChecker) {
+ mParams.mIsSpellChecker = isSpellChecker;
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, mEditorInfo);
+ null, NO_MICROPHONE_COMPAT, mParams.mEditorInfo);
final boolean noMicrophone = InputAttributes.inPrivateImeOptions(
- mPackageName, NO_MICROPHONE, mEditorInfo)
+ mPackageName, NO_MICROPHONE, mParams.mEditorInfo)
|| deprecatedNoMicrophone;
mParams.mVoiceKeyEnabled = voiceKeyEnabled && !noMicrophone;
mParams.mVoiceKeyOnMain = voiceKeyOnMain;
@@ -291,8 +282,6 @@ public final class KeyboardLayoutSet {
}
public KeyboardLayoutSet build() {
- if (mParams.mOrientation == Configuration.ORIENTATION_UNDEFINED)
- throw new RuntimeException("Screen geometry is not specified");
if (mParams.mSubtype == null)
throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
final String packageName = mResources.getResourcePackageName(
@@ -416,48 +405,4 @@ public final class KeyboardLayoutSet {
}
}
}
-
- public static KeyboardLayoutSet createKeyboardSetForSpellChecker(final Context context,
- final String locale, final String layout) {
- final InputMethodSubtype subtype =
- AdditionalSubtype.createAdditionalSubtype(locale, layout, null);
- return createKeyboardSet(context, subtype, SPELLCHECKER_DUMMY_KEYBOARD_WIDTH,
- SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT, false);
- }
-
- @UsedForTesting
- public static KeyboardLayoutSet createKeyboardSetForTest(final Context context,
- final InputMethodSubtype subtype, final int orientation,
- final boolean testCasesHaveTouchCoordinates) {
- final DisplayMetrics dm = context.getResources().getDisplayMetrics();
- final int width;
- final int height;
- if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
- width = Math.max(dm.widthPixels, dm.heightPixels);
- height = Math.min(dm.widthPixels, dm.heightPixels);
- } else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
- width = Math.min(dm.widthPixels, dm.heightPixels);
- height = Math.max(dm.widthPixels, dm.heightPixels);
- } else {
- throw new RuntimeException("Orientation should be ORIENTATION_LANDSCAPE or "
- + "ORIENTATION_PORTRAIT: orientation=" + orientation);
- }
- return createKeyboardSet(context, subtype, width, height, testCasesHaveTouchCoordinates);
- }
-
- private static KeyboardLayoutSet createKeyboardSet(final Context context,
- final InputMethodSubtype subtype, final int width, final int height,
- final boolean testCasesHaveTouchCoordinates) {
- final EditorInfo editorInfo = new EditorInfo();
- editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
- final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
- context, editorInfo);
- builder.setScreenGeometry(width, height);
- builder.setSubtype(subtype);
- if (!testCasesHaveTouchCoordinates) {
- // For spell checker and tests
- builder.disableTouchPositionCorrectionData();
- }
- return builder.build();
- }
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 4323f7171..cc1ffd183 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -20,7 +20,6 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
-import android.util.DisplayMetrics;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -29,18 +28,17 @@ import android.view.inputmethod.EditorInfo;
import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
import com.android.inputmethod.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException;
-import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
import com.android.inputmethod.keyboard.internal.KeyboardState;
-import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
import com.android.inputmethod.latin.InputView;
import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.RichInputMethodManager;
-import com.android.inputmethod.latin.Settings;
-import com.android.inputmethod.latin.SettingsValues;
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();
@@ -60,19 +58,17 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
private static final KeyboardTheme[] KEYBOARD_THEMES = {
- new KeyboardTheme(0, R.style.KeyboardTheme),
- new KeyboardTheme(1, R.style.KeyboardTheme_HighContrast),
- new KeyboardTheme(6, R.style.KeyboardTheme_Stone),
- new KeyboardTheme(7, R.style.KeyboardTheme_Stone_Bold),
- new KeyboardTheme(8, R.style.KeyboardTheme_Gingerbread),
- new KeyboardTheme(5, R.style.KeyboardTheme_IceCreamSandwich),
+ new KeyboardTheme(0, R.style.KeyboardTheme_ICS),
+ new KeyboardTheme(1, R.style.KeyboardTheme_GB),
};
private SubtypeSwitcher mSubtypeSwitcher;
private SharedPreferences mPrefs;
private InputView mCurrentInputView;
+ private View mMainKeyboardFrame;
private MainKeyboardView mKeyboardView;
+ private EmojiKeyboardView mEmojiKeyboardView;
private LatinIME mLatinIME;
private Resources mResources;
@@ -123,8 +119,9 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
} catch (NumberFormatException e) {
// Format error, keyboard theme is default to 0.
}
- Log.w(TAG, "Illegal keyboard theme in preference: " + themeIndex + ", default to 0");
- return KEYBOARD_THEMES[0];
+ Log.w(TAG, "Illegal keyboard theme in preference: " + themeIndex + ", default to "
+ + defaultIndex);
+ return KEYBOARD_THEMES[Integer.valueOf(defaultIndex)];
}
private void setContextThemeWrapper(final Context context, final KeyboardTheme keyboardTheme) {
@@ -139,12 +136,13 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
mThemeContext, editorInfo);
final Resources res = mThemeContext.getResources();
- final DisplayMetrics dm = res.getDisplayMetrics();
- builder.setScreenGeometry(dm.widthPixels, dm.heightPixels);
+ final int keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res);
+ final int keyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res);
+ builder.setKeyboardGeometry(keyboardWidth, keyboardHeight);
builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype());
builder.setOptions(
settingsValues.isVoiceKeyEnabled(editorInfo),
- settingsValues.isVoiceKeyOnMain(),
+ true /* always show a voice key on the main keyboard */,
settingsValues.isLanguageSwitchKeyEnabled());
mKeyboardLayoutSet = builder.build();
try {
@@ -171,6 +169,8 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
private void setKeyboard(final Keyboard keyboard) {
+ // Make {@link MainKeyboardView} visible and hide {@link EmojiKeyboardView}.
+ setMainKeyboardFrame();
final MainKeyboardView keyboardView = mKeyboardView;
final Keyboard oldKeyboard = keyboardView.getKeyboard();
keyboardView.setKeyboard(keyboard);
@@ -210,7 +210,6 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
public void onPressKey(final int code, final boolean isSinglePointer) {
- hapticAndAudioFeedback(code);
mState.onPressKey(code, isSinglePointer, mLatinIME.getCurrentAutoCapsState());
}
@@ -258,6 +257,18 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_SYMBOLS));
}
+ private void setMainKeyboardFrame() {
+ mMainKeyboardFrame.setVisibility(View.VISIBLE);
+ mEmojiKeyboardView.setVisibility(View.GONE);
+ }
+
+ // Implements {@link KeyboardState.SwitchActions}.
+ @Override
+ public void setEmojiKeyboard() {
+ mMainKeyboardFrame.setVisibility(View.GONE);
+ mEmojiKeyboardView.setVisibility(View.VISIBLE);
+ }
+
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void setSymbolsShiftedKeyboard() {
@@ -276,8 +287,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
public void startDoubleTapShiftKeyTimer() {
final MainKeyboardView keyboardView = getMainKeyboardView();
if (keyboardView != null) {
- final TimerProxy timer = keyboardView.getTimerProxy();
- timer.startDoubleTapShiftKeyTimer();
+ keyboardView.startDoubleTapShiftKeyTimer();
}
}
@@ -286,8 +296,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
public void cancelDoubleTapShiftKeyTimer() {
final MainKeyboardView keyboardView = getMainKeyboardView();
if (keyboardView != null) {
- final TimerProxy timer = keyboardView.getTimerProxy();
- timer.cancelDoubleTapShiftKeyTimer();
+ keyboardView.cancelDoubleTapShiftKeyTimer();
}
}
@@ -295,15 +304,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
@Override
public boolean isInDoubleTapShiftKeyTimeout() {
final MainKeyboardView keyboardView = getMainKeyboardView();
- return (keyboardView != null)
- ? keyboardView.getTimerProxy().isInDoubleTapShiftKeyTimeout() : false;
- }
-
- private void hapticAndAudioFeedback(final int code) {
- if (mKeyboardView == null || mKeyboardView.isInSlidingKeyInput()) {
- return;
- }
- AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(code, mKeyboardView);
+ return keyboardView != null && keyboardView.isInDoubleTapShiftKeyTimeout();
}
/**
@@ -313,6 +314,24 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
mState.onCodeInput(code, mLatinIME.getCurrentAutoCapsState());
}
+ public boolean isShowingEmojiKeyboard() {
+ return mEmojiKeyboardView.getVisibility() == View.VISIBLE;
+ }
+
+ public boolean isShowingMoreKeysPanel() {
+ if (isShowingEmojiKeyboard()) {
+ return false;
+ }
+ return mKeyboardView.isShowingMoreKeysPanel();
+ }
+
+ public View getVisibleKeyboardView() {
+ if (isShowingEmojiKeyboard()) {
+ return mEmojiKeyboardView;
+ }
+ return mKeyboardView;
+ }
+
public MainKeyboardView getMainKeyboardView() {
return mKeyboardView;
}
@@ -325,13 +344,16 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
setContextThemeWrapper(mLatinIME, mKeyboardTheme);
mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
R.layout.input_view, null);
+ mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame);
+ mEmojiKeyboardView = (EmojiKeyboardView)mCurrentInputView.findViewById(
+ R.id.emoji_keyboard_view);
mKeyboardView = (MainKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view);
- if (isHardwareAcceleratedDrawingEnabled) {
- mKeyboardView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
- // TODO: Should use LAYER_TYPE_SOFTWARE when hardware acceleration is off?
- }
+ mKeyboardView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled);
mKeyboardView.setKeyboardActionListener(mLatinIME);
+ mEmojiKeyboardView.setHardwareAcceleratedDrawingEnabled(
+ isHardwareAcceleratedDrawingEnabled);
+ mEmojiKeyboardView.setKeyboardActionListener(mLatinIME);
// This always needs to be set since the accessibility state can
// potentially change without the input view being re-created.
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 7941fcba2..aeb9e67b2 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -26,17 +26,19 @@ import android.graphics.Paint.Align;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
+import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import com.android.inputmethod.keyboard.internal.KeyDrawParams;
import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.TypefaceUtils;
import com.android.inputmethod.research.ResearchLogger;
import java.util.HashSet;
@@ -153,6 +155,12 @@ public class KeyboardView extends View {
Color.red(color), Color.green(color), Color.blue(color));
}
+ public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
+ if (!enabled) return;
+ // TODO: Should use LAYER_TYPE_SOFTWARE when hardware acceleration is off?
+ setLayerType(LAYER_TYPE_HARDWARE, null);
+ }
+
/**
* Attaches a keyboard to this view. The keyboard can be switched at any time and the
* view will re-layout itself to accommodate the keyboard.
@@ -189,13 +197,14 @@ public class KeyboardView extends View {
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
- if (mKeyboard != null) {
- // The main keyboard expands to the display width.
- final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
- setMeasuredDimension(widthMeasureSpec, height);
- } else {
+ if (mKeyboard == null) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ return;
}
+ // The main keyboard expands to the entire this {@link KeyboardView}.
+ final int width = mKeyboard.mOccupiedWidth + getPaddingLeft() + getPaddingRight();
+ final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom();
+ setMeasuredDimension(width, height);
}
@Override
@@ -257,9 +266,9 @@ public class KeyboardView extends View {
mClipRegion.setEmpty();
for (final Key key : mInvalidatedKeys) {
if (mKeyboard.hasKey(key)) {
- final int x = key.mX + getPaddingLeft();
- final int y = key.mY + getPaddingTop();
- mWorkingRect.set(x, y, x + key.mWidth, y + key.mHeight);
+ final int x = key.getX() + getPaddingLeft();
+ final int y = key.getY() + getPaddingTop();
+ mWorkingRect.set(x, y, x + key.getWidth(), y + key.getHeight());
mClipRegion.union(mWorkingRect);
}
}
@@ -277,7 +286,7 @@ public class KeyboardView extends View {
// TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
if (drawAllKeys || isHardwareAccelerated) {
// Draw all keys.
- for (final Key key : mKeyboard.mKeys) {
+ for (final Key key : mKeyboard.getKeys()) {
onDrawKey(key, canvas, paint);
}
} else {
@@ -302,11 +311,11 @@ public class KeyboardView extends View {
private void onDrawKey(final Key key, final Canvas canvas, final Paint paint) {
final int keyDrawX = key.getDrawX() + getPaddingLeft();
- final int keyDrawY = key.mY + getPaddingTop();
+ final int keyDrawY = key.getY() + getPaddingTop();
canvas.translate(keyDrawX, keyDrawY);
final int keyHeight = mKeyboard.mMostCommonKeyHeight - mKeyboard.mVerticalGap;
- final KeyVisualAttributes attr = key.mKeyVisualAttributes;
+ final KeyVisualAttributes attr = key.getVisualAttributes();
final KeyDrawParams params = mKeyDrawParams.mayCloneAndUpdateParams(keyHeight, attr);
params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE;
@@ -322,7 +331,7 @@ public class KeyboardView extends View {
protected void onDrawKeyBackground(final Key key, final Canvas canvas) {
final Rect padding = mKeyBackgroundPadding;
final int bgWidth = key.getDrawWidth() + padding.left + padding.right;
- final int bgHeight = key.mHeight + padding.top + padding.bottom;
+ final int bgHeight = key.getHeight() + padding.top + padding.bottom;
final int bgX = -padding.left;
final int bgY = -padding.top;
final int[] drawableState = key.getCurrentDrawableState();
@@ -344,7 +353,7 @@ public class KeyboardView extends View {
protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
final KeyDrawParams params) {
final int keyWidth = key.getDrawWidth();
- final int keyHeight = key.mHeight;
+ final int keyHeight = key.getHeight();
final float centerX = keyWidth * 0.5f;
final float centerY = keyHeight * 0.5f;
@@ -355,8 +364,8 @@ public class KeyboardView extends View {
// Draw key label.
final Drawable icon = key.getIcon(mKeyboard.mIconsSet, params.mAnimAlpha);
float positionX = centerX;
- if (key.mLabel != null) {
- final String label = key.mLabel;
+ final String label = key.getLabel();
+ if (label != null) {
paint.setTypeface(key.selectTypeface(params));
paint.setTextSize(key.selectTextSize(params));
final float labelCharHeight = TypefaceUtils.getCharHeight(
@@ -433,10 +442,12 @@ public class KeyboardView extends View {
}
// Draw hint label.
- if (key.mHintLabel != null) {
- final String hintLabel = key.mHintLabel;
+ final String hintLabel = key.getHintLabel();
+ if (hintLabel != null) {
paint.setTextSize(key.selectHintTextSize(params));
paint.setColor(key.selectHintTextColor(params));
+ // TODO: Should add a way to specify type face for hint letters
+ paint.setTypeface(Typeface.DEFAULT_BOLD);
blendAlpha(paint, params.mAnimAlpha);
final float hintX, hintY;
if (key.hasHintLabel()) {
@@ -457,9 +468,13 @@ public class KeyboardView extends View {
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);
hintX = keyWidth - mKeyHintLetterPadding
- - TypefaceUtils.getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint)
- / 2.0f;
+ - Math.max(keyNumericHintLabelReferenceCharWidth, keyHintLabelStringWidth)
+ / 2.0f;
hintY = -paint.ascent();
paint.setTextAlign(Align.CENTER);
}
@@ -473,7 +488,7 @@ public class KeyboardView extends View {
}
// Draw key icon.
- if (key.mLabel == null && icon != null) {
+ if (label == null && icon != null) {
final int iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth);
final int iconHeight = icon.getIntrinsicHeight();
final int iconX, alignX;
@@ -497,7 +512,7 @@ public class KeyboardView extends View {
}
}
- if (key.hasPopupHint() && key.mMoreKeys != null) {
+ if (key.hasPopupHint() && key.getMoreKeys() != null) {
drawKeyPopupHint(key, canvas, paint, params);
}
}
@@ -506,7 +521,7 @@ public class KeyboardView extends View {
protected void drawKeyPopupHint(final Key key, final Canvas canvas, final Paint paint,
final KeyDrawParams params) {
final int keyWidth = key.getDrawWidth();
- final int keyHeight = key.mHeight;
+ final int keyHeight = key.getHeight();
paint.setTypeface(params.mTypeface);
paint.setTextSize(params.mHintLetterSize);
@@ -594,9 +609,9 @@ public class KeyboardView extends View {
if (mInvalidateAllKeys) return;
if (key == null) return;
mInvalidatedKeys.add(key);
- final int x = key.mX + getPaddingLeft();
- final int y = key.mY + getPaddingTop();
- invalidate(x, y, x + key.mWidth, y + key.mHeight);
+ final int x = key.getX() + getPaddingLeft();
+ final int y = key.getY() + getPaddingTop();
+ invalidate(x, y, x + key.getWidth(), y + key.getHeight());
}
@Override
@@ -604,4 +619,8 @@ public class KeyboardView extends View {
super.onDetachedFromWindow();
freeOffscreenBuffer();
}
+
+ public void deallocateMemory() {
+ freeOffscreenBuffer();
+ }
}
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 7f335027f..13db47004 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -21,7 +21,6 @@ import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
-import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -54,28 +53,24 @@ import com.android.inputmethod.keyboard.internal.GestureFloatingPreviewText;
import com.android.inputmethod.keyboard.internal.GestureTrailsPreview;
import com.android.inputmethod.keyboard.internal.KeyDrawParams;
import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams;
+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.TouchScreenRegulator;
-import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.CoordinateUtils;
-import com.android.inputmethod.latin.DebugSettings;
-import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResourceUtils;
-import com.android.inputmethod.latin.Settings;
-import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
-import com.android.inputmethod.latin.StringUtils;
-import com.android.inputmethod.latin.SubtypeLocale;
import com.android.inputmethod.latin.SuggestedWords;
-import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils;
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.TypefaceUtils;
+import com.android.inputmethod.latin.utils.UsabilityStudyLogUtils;
+import com.android.inputmethod.latin.utils.ViewLayoutUtils;
import com.android.inputmethod.research.ResearchLogger;
-import java.util.Locale;
import java.util.WeakHashMap;
/**
@@ -120,13 +115,9 @@ import java.util.WeakHashMap;
* @attr ref R.styleable#MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration
*/
public final class MainKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
- PointerTracker.DrawingProxy, MoreKeysPanel.Controller,
- TouchScreenRegulator.ProcessMotionEvent {
+ PointerTracker.DrawingProxy, MoreKeysPanel.Controller {
private static final String TAG = MainKeyboardView.class.getSimpleName();
- // TODO: Kill process when the usability study mode was changed.
- private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy;
-
/** Listener for {@link KeyboardActionListener}. */
private KeyboardActionListener mKeyboardActionListener;
@@ -164,7 +155,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
private final SlidingKeyInputPreview mSlidingKeyInputPreview;
// Key preview
- private static final int PREVIEW_ALPHA = 240;
private final int mKeyPreviewLayoutId;
private final int mKeyPreviewOffset;
private final int mKeyPreviewHeight;
@@ -188,14 +178,11 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
// TODO: Make this parameter customizable by user via settings.
private int mGestureFloatingPreviewTextLingerTimeout;
- private final TouchScreenRegulator mTouchScreenRegulator;
-
private KeyDetector mKeyDetector;
- private final boolean mHasDistinctMultitouch;
- private int mOldPointerCount = 1;
- private Key mOldKey;
+ private final NonDistinctMultitouchHelper mNonDistinctMultitouchHelper;
private final KeyTimerHandler mKeyTimerHandler;
+ private final int mLanguageOnSpacebarHorizontalMargin;
private static final class KeyTimerHandler extends StaticInnerHandlerWrapper<MainKeyboardView>
implements TimerProxy {
@@ -205,9 +192,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
private static final int MSG_DOUBLE_TAP_SHIFT_KEY = 3;
private static final int MSG_UPDATE_BATCH_INPUT = 4;
- private final int mKeyRepeatStartTimeout;
- private final int mKeyRepeatInterval;
- private final int mLongPressShiftLockTimeout;
private final int mIgnoreAltCodeKeyTimeout;
private final int mGestureRecognitionUpdateTime;
@@ -215,12 +199,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
final TypedArray mainKeyboardViewAttr) {
super(outerInstance);
- mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt(
- R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0);
- mKeyRepeatInterval = mainKeyboardViewAttr.getInt(
- R.styleable.MainKeyboardView_keyRepeatInterval, 0);
- mLongPressShiftLockTimeout = mainKeyboardViewAttr.getInt(
- R.styleable.MainKeyboardView_longPressShiftLockTimeout, 0);
mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt(
R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0);
mGestureRecognitionUpdateTime = mainKeyboardViewAttr.getInt(
@@ -239,13 +217,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
startWhileTypingFadeinAnimation(keyboardView);
break;
case MSG_REPEAT_KEY:
- final Key currentKey = tracker.getKey();
- if (currentKey != null && currentKey.mCode == msg.arg1) {
- tracker.onRepeatKey(currentKey);
- AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(
- currentKey.mCode, keyboardView);
- startKeyRepeatTimer(tracker, mKeyRepeatInterval);
- }
+ tracker.onKeyRepeat(msg.arg1 /* code */, msg.arg2 /* repeatCount */);
break;
case MSG_LONGPRESS_KEY:
keyboardView.onLongPress(tracker);
@@ -257,17 +229,15 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
}
- private void startKeyRepeatTimer(final PointerTracker tracker, final long delay) {
+ @Override
+ public void startKeyRepeatTimer(final PointerTracker tracker, final int repeatCount,
+ final int delay) {
final Key key = tracker.getKey();
- if (key == null) {
+ if (key == null || delay == 0) {
return;
}
- sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, key.mCode, 0, tracker), delay);
- }
-
- @Override
- public void startKeyRepeatTimer(final PointerTracker tracker) {
- startKeyRepeatTimer(tracker, mKeyRepeatStartTimeout);
+ sendMessageDelayed(
+ obtainMessage(MSG_REPEAT_KEY, key.getCode(), repeatCount, tracker), delay);
}
public void cancelKeyRepeatTimer() {
@@ -280,31 +250,10 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
@Override
- public void startLongPressTimer(final PointerTracker tracker) {
+ public void startLongPressTimer(final PointerTracker tracker, final int delay) {
cancelLongPressTimer();
- if (tracker == null) {
- return;
- }
- final Key key = tracker.getKey();
- final int delay;
- switch (key.mCode) {
- case Constants.CODE_SHIFT:
- delay = mLongPressShiftLockTimeout;
- break;
- default:
- final int longpressTimeout =
- Settings.getInstance().getCurrent().mKeyLongpressTimeout;
- if (tracker.isInSlidingKeyInputFromModifier()) {
- // We use longer timeout for sliding finger input started from the modifier key.
- delay = longpressTimeout * 3;
- } else {
- delay = longpressTimeout;
- }
- break;
- }
- if (delay > 0) {
- sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay);
- }
+ if (delay <= 0) return;
+ sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay);
}
@Override
@@ -349,7 +298,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
final MainKeyboardView keyboardView = getOuterInstance();
// When user hits the space or the enter key, just cancel the while-typing timer.
- final int typedCode = typedKey.mCode;
+ final int typedCode = typedKey.getCode();
if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) {
if (isTyping) {
startWhileTypingFadeinAnimation(keyboardView);
@@ -475,19 +424,16 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
public MainKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
- mTouchScreenRegulator = new TouchScreenRegulator(context, this);
-
+ PointerTracker.init(getResources());
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final boolean forceNonDistinctMultitouch = prefs.getBoolean(
DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH, false);
final boolean hasDistinctMultitouch = context.getPackageManager()
- .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
- mHasDistinctMultitouch = hasDistinctMultitouch && !forceNonDistinctMultitouch;
- final Resources res = getResources();
- final boolean needsPhantomSuddenMoveEventHack = Boolean.parseBoolean(
- ResourceUtils.getDeviceOverrideValue(
- res, R.array.phantom_sudden_move_event_device_list));
- PointerTracker.init(needsPhantomSuddenMoveEventHack);
+ .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)
+ && !forceNonDistinctMultitouch;
+ mNonDistinctMultitouchHelper = hasDistinctMultitouch ? null
+ : new NonDistinctMultitouchHelper();
+
mPreviewPlacerView = new PreviewPlacerView(context, attrs);
final TypedArray mainKeyboardViewAttr = context.obtainStyledAttributes(
@@ -564,6 +510,17 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
altCodeKeyWhileTypingFadeoutAnimatorResId, this);
mAltCodeKeyWhileTypingFadeinAnimator = loadObjectAnimator(
altCodeKeyWhileTypingFadeinAnimatorResId, this);
+
+ mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
+
+ mLanguageOnSpacebarHorizontalMargin =
+ (int) getResources().getDimension(R.dimen.language_on_spacebar_horizontal_margin);
+ }
+
+ @Override
+ public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
+ super.setHardwareAcceleratedDrawingEnabled(enabled);
+ mPreviewPlacerView.setHardwareAcceleratedDrawingEnabled(enabled);
}
private ObjectAnimator loadObjectAnimator(final int resId, final Object target) {
@@ -655,7 +612,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
mKeyDetector.setKeyboard(
keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
PointerTracker.setKeyDetector(mKeyDetector);
- mTouchScreenRegulator.setKeyboardGeometry(keyboard.mOccupiedWidth);
mMoreKeysKeyboardCache.clear();
mSpaceKey = keyboard.getKey(Constants.CODE_SPACE);
@@ -664,7 +620,8 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
mSpacebarTextSize = keyHeight * mSpacebarTextRatio;
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.mainKeyboardView_setKeyboard(keyboard);
+ final int orientation = getContext().getResources().getConfiguration().orientation;
+ ResearchLogger.mainKeyboardView_setKeyboard(keyboard, orientation);
}
// This always needs to be set since the accessibility state can
@@ -684,7 +641,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
mKeyPreviewLingerTimeout = delay;
}
-
private void locatePreviewPlacerView() {
if (mPreviewPlacerView.getParent() != null) {
return;
@@ -777,8 +733,6 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
private static final int STATE_RIGHT = 2;
private static final int STATE_NORMAL = 0;
private static final int STATE_HAS_MOREKEYS = 1;
- private static final int[] KEY_PREVIEW_BACKGROUND_DEFAULT_STATE =
- KEY_PREVIEW_BACKGROUND_STATE_TABLE[STATE_MIDDLE][STATE_NORMAL];
@Override
public void showKeyPreview(final PointerTracker tracker) {
@@ -855,13 +809,12 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
// The key preview is placed vertically above the top edge of the parent key with an
// arbitrary offset.
- final int previewY = key.mY - previewHeight + mKeyPreviewOffset
+ final int previewY = key.getY() - previewHeight + mKeyPreviewOffset
+ CoordinateUtils.y(mOriginCoords);
if (background != null) {
- final int hasMoreKeys = (key.mMoreKeys != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL;
+ final int hasMoreKeys = (key.getMoreKeys() != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL;
background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]);
- background.setAlpha(PREVIEW_ALPHA);
}
ViewLayoutUtils.placeViewAt(
previewText, previewX, previewY, previewWidth, previewHeight);
@@ -888,10 +841,10 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
mSlidingKeyInputPreview.dismissSlidingKeyInputPreview();
}
- public void setGesturePreviewMode(final boolean drawsGestureTrail,
- final boolean drawsGestureFloatingPreviewText) {
- mGestureFloatingPreviewText.setPreviewEnabled(drawsGestureFloatingPreviewText);
- mGestureTrailsPreview.setPreviewEnabled(drawsGestureTrail);
+ private void setGesturePreviewMode(final boolean isGestureTrailEnabled,
+ final boolean isGestureFloatingPreviewTextEnabled) {
+ mGestureFloatingPreviewText.setPreviewEnabled(isGestureFloatingPreviewTextEnabled);
+ mGestureTrailsPreview.setPreviewEnabled(isGestureTrailEnabled);
}
public void showGestureFloatingPreviewText(final SuggestedWords suggestedWords) {
@@ -905,9 +858,12 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
@Override
- public void showGestureTrail(final PointerTracker tracker) {
+ public void showGestureTrail(final PointerTracker tracker,
+ final boolean showsFloatingPreviewText) {
locatePreviewPlacerView();
- mGestureFloatingPreviewText.setPreviewPosition(tracker);
+ if (showsFloatingPreviewText) {
+ mGestureFloatingPreviewText.setPreviewPosition(tracker);
+ }
mGestureTrailsPreview.setPreviewPosition(tracker);
}
@@ -916,8 +872,12 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
PointerTracker.setMainDictionaryAvailability(mainDictionaryAvailable);
}
- public void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) {
- PointerTracker.setGestureHandlingEnabledByUser(gestureHandlingEnabledByUser);
+ public void setGestureHandlingEnabledByUser(final boolean isGestureHandlingEnabledByUser,
+ final boolean isGestureTrailEnabled,
+ final boolean isGestureFloatingPreviewTextEnabled) {
+ PointerTracker.setGestureHandlingEnabledByUser(isGestureHandlingEnabledByUser);
+ setGesturePreviewMode(isGestureHandlingEnabledByUser && isGestureTrailEnabled,
+ isGestureHandlingEnabledByUser && isGestureFloatingPreviewTextEnabled);
}
@Override
@@ -944,7 +904,7 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
private MoreKeysPanel onCreateMoreKeysPanel(final Key key, final Context context) {
- if (key.mMoreKeys == null) {
+ if (key.getMoreKeys() == null) {
return null;
}
Keyboard moreKeysKeyboard = mMoreKeysKeyboardCache.get(key);
@@ -977,39 +937,28 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.mainKeyboardView_onLongPress();
}
- final int code = key.mCode;
+ final KeyboardActionListener listener = mKeyboardActionListener;
if (key.hasNoPanelAutoMoreKey()) {
- final int embeddedCode = key.mMoreKeys[0].mCode;
+ final int moreKeyCode = key.getMoreKeys()[0].mCode;
tracker.onLongPressed();
- invokeCodeInput(embeddedCode);
- invokeReleaseKey(code);
- AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(code, this);
+ listener.onPressKey(moreKeyCode, 0 /* repeatCount */, true /* isSinglePointer */);
+ listener.onCodeInput(moreKeyCode,
+ Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+ listener.onReleaseKey(moreKeyCode, false /* withSliding */);
return;
}
+ final int code = key.getCode();
if (code == Constants.CODE_SPACE || code == Constants.CODE_LANGUAGE_SWITCH) {
// Long pressing the space key invokes IME switcher dialog.
- if (invokeCustomRequest(LatinIME.CODE_SHOW_INPUT_METHOD_PICKER)) {
+ if (listener.onCustomRequest(Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER)) {
tracker.onLongPressed();
- invokeReleaseKey(code);
+ listener.onReleaseKey(code, false /* withSliding */);
return;
}
}
openMoreKeysPanel(key, tracker);
}
- private boolean invokeCustomRequest(final int requestCode) {
- return mKeyboardActionListener.onCustomRequest(requestCode);
- }
-
- private void invokeCodeInput(final int code) {
- mKeyboardActionListener.onCodeInput(
- code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
- }
-
- private void invokeReleaseKey(final int code) {
- mKeyboardActionListener.onReleaseKey(code, false);
- }
-
private void openMoreKeysPanel(final Key key, final PointerTracker tracker) {
final MoreKeysPanel moreKeysPanel = onCreateMoreKeysPanel(key, getContext());
if (moreKeysPanel == null) {
@@ -1024,17 +973,15 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
// keys keyboard is placed at the touch point of the parent key.
final int pointX = (mConfigShowMoreKeysKeyboardAtTouchedPoint && !keyPreviewEnabled)
? CoordinateUtils.x(lastCoords)
- : key.mX + key.mWidth / 2;
+ : key.getX() + key.getWidth() / 2;
// The more keys keyboard is usually vertically aligned with the top edge of the parent key
// (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically
// 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.mY + mKeyPreviewDrawParams.mPreviewVisibleOffset;
+ final int pointY = key.getY() + mKeyPreviewDrawParams.mPreviewVisibleOffset;
moreKeysPanel.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener);
- final int translatedX = moreKeysPanel.translateX(CoordinateUtils.x(lastCoords));
- final int translatedY = moreKeysPanel.translateY(CoordinateUtils.y(lastCoords));
- tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
+ tracker.onShowMoreKeysPanel(moreKeysPanel);
}
public boolean isInSlidingKeyInput() {
@@ -1047,8 +994,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
@Override
public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
locatePreviewPlacerView();
- if (isShowingMoreKeysPanel()) {
- onDismissMoreKeysPanel();
+ // TODO: Remove this check
+ if (panel.isShowingInParent()) {
+ panel.dismissMoreKeysPanel();
}
mPreviewPlacerView.addView(panel.getContainerView());
mMoreKeysPanel = panel;
@@ -1060,19 +1008,29 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
@Override
- public void onCancelMoreKeysPanel() {
+ public void onCancelMoreKeysPanel(final MoreKeysPanel panel) {
PointerTracker.dismissAllMoreKeysPanels();
}
@Override
- public boolean onDismissMoreKeysPanel() {
+ public void onDismissMoreKeysPanel(final MoreKeysPanel panel) {
dimEntireKeyboard(false /* dimmed */);
if (isShowingMoreKeysPanel()) {
mPreviewPlacerView.removeView(mMoreKeysPanel.getContainerView());
mMoreKeysPanel = null;
- return true;
}
- return false;
+ }
+
+ public void startDoubleTapShiftKeyTimer() {
+ mKeyTimerHandler.startDoubleTapShiftKeyTimer();
+ }
+
+ public void cancelDoubleTapShiftKeyTimer() {
+ mKeyTimerHandler.cancelDoubleTapShiftKeyTimer();
+ }
+
+ public boolean isInDoubleTapShiftKeyTimeout() {
+ return mKeyTimerHandler.isInDoubleTapShiftKeyTimeout();
}
@Override
@@ -1088,149 +1046,46 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
if (getKeyboard() == null) {
return false;
}
- return mTouchScreenRegulator.onTouchEvent(me);
- }
-
- @Override
- public boolean processMotionEvent(final MotionEvent me) {
- final boolean nonDistinctMultitouch = !mHasDistinctMultitouch;
- final int action = me.getActionMasked();
- final int pointerCount = me.getPointerCount();
- final int oldPointerCount = mOldPointerCount;
- mOldPointerCount = pointerCount;
-
- // TODO: cleanup this code into a multi-touch to single-touch event converter class?
- // If the device does not have distinct multi-touch support panel, ignore all multi-touch
- // events except a transition from/to single-touch.
- if (nonDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
+ 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();
+ }
+ // Non distinct multitouch screen support
+ mNonDistinctMultitouchHelper.processMotionEvent(me, this);
return true;
}
+ return processMotionEvent(me);
+ }
- final long eventTime = me.getEventTime();
- final int index = me.getActionIndex();
- final int id = me.getPointerId(index);
- final int x = (int)me.getX(index);
- final int y = (int)me.getY(index);
-
- // TODO: This might be moved to the tracker.processMotionEvent() call below.
- if (ENABLE_USABILITY_STUDY_LOG && action != MotionEvent.ACTION_MOVE) {
- writeUsabilityStudyLog(me, action, eventTime, index, id, x, y);
+ public boolean processMotionEvent(final MotionEvent me) {
+ if (LatinImeLogger.sUsabilityStudy) {
+ UsabilityStudyLogUtils.writeMotionEvent(me);
}
- // TODO: This should be moved to the tracker.processMotionEvent() call below.
// Currently the same "move" event is being logged twice.
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.mainKeyboardView_processMotionEvent(
- me, action, eventTime, index, id, x, y);
- }
-
- if (mKeyTimerHandler.isInKeyRepeat()) {
- final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
- // Key repeating timer will be canceled if 2 or more keys are in action, and current
- // event (UP or DOWN) is non-modifier key.
- if (pointerCount > 1 && !tracker.isModifier()) {
- mKeyTimerHandler.cancelKeyRepeatTimer();
- }
- // Up event will pass through.
- }
-
- // TODO: cleanup this code into a multi-touch to single-touch event converter class?
- // Translate mutli-touch event to single-touch events on the device that has no distinct
- // multi-touch panel.
- if (nonDistinctMultitouch) {
- // Use only main (id=0) pointer tracker.
- final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
- if (pointerCount == 1 && oldPointerCount == 2) {
- // Multi-touch to single touch transition.
- // Send a down event for the latest pointer if the key is different from the
- // previous key.
- final Key newKey = tracker.getKeyOn(x, y);
- if (mOldKey != newKey) {
- tracker.onDownEvent(x, y, eventTime, this);
- if (action == MotionEvent.ACTION_UP) {
- tracker.onUpEvent(x, y, eventTime);
- }
- }
- } else if (pointerCount == 2 && oldPointerCount == 1) {
- // Single-touch to multi-touch transition.
- // Send an up event for the last pointer.
- final int[] lastCoords = CoordinateUtils.newInstance();
- mOldKey = tracker.getKeyOn(
- CoordinateUtils.x(lastCoords), CoordinateUtils.y(lastCoords));
- tracker.onUpEvent(
- CoordinateUtils.x(lastCoords), CoordinateUtils.y(lastCoords), eventTime);
- } else if (pointerCount == 1 && oldPointerCount == 1) {
- tracker.processMotionEvent(action, x, y, eventTime, this);
- } else {
- Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
- + " (old " + oldPointerCount + ")");
- }
- return true;
- }
-
- if (action == MotionEvent.ACTION_MOVE) {
- for (int i = 0; i < pointerCount; i++) {
- final int pointerId = me.getPointerId(i);
- final PointerTracker tracker = PointerTracker.getPointerTracker(
- pointerId, this);
- final int px = (int)me.getX(i);
- final int py = (int)me.getY(i);
- tracker.onMoveEvent(px, py, eventTime, me);
- if (ENABLE_USABILITY_STUDY_LOG) {
- writeUsabilityStudyLog(me, action, eventTime, i, pointerId, px, py);
- }
- // TODO: This seems to be no longer necessary, and confusing because it leads to
- // duplicate MotionEvents being recorded.
- // if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- // ResearchLogger.mainKeyboardView_processMotionEvent(
- // me, action, eventTime, i, pointerId, px, py);
- // }
- }
- } else {
- final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
- tracker.processMotionEvent(action, x, y, eventTime, this);
+ ResearchLogger.mainKeyboardView_processMotionEvent(me);
}
+ final int index = me.getActionIndex();
+ final int id = me.getPointerId(index);
+ final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
+ tracker.processMotionEvent(me, this);
return true;
}
- private static void writeUsabilityStudyLog(final MotionEvent me, final int action,
- final long eventTime, final int index, final int id, final int x, final int y) {
- final String eventTag;
- switch (action) {
- case MotionEvent.ACTION_UP:
- eventTag = "[Up]";
- break;
- case MotionEvent.ACTION_DOWN:
- eventTag = "[Down]";
- break;
- case MotionEvent.ACTION_POINTER_UP:
- eventTag = "[PointerUp]";
- break;
- case MotionEvent.ACTION_POINTER_DOWN:
- eventTag = "[PointerDown]";
- break;
- case MotionEvent.ACTION_MOVE:
- eventTag = "[Move]";
- break;
- default:
- eventTag = "[Action" + action + "]";
- break;
- }
- final float size = me.getSize(index);
- final float pressure = me.getPressure(index);
- UsabilityStudyLogUtils.getInstance().write(
- eventTag + eventTime + "," + id + "," + x + "," + y + "," + size + "," + pressure);
- }
-
- public void cancelAllMessages() {
+ public void cancelAllOngoingEvents() {
mKeyTimerHandler.cancelAllMessages();
mDrawingHandler.cancelAllMessages();
+ dismissAllKeyPreviews();
+ dismissGestureFloatingPreviewText();
+ dismissSlidingKeyInputPreview();
+ PointerTracker.dismissAllMoreKeysPanels();
+ PointerTracker.cancelAllPointerTrackers();
}
public void closing() {
- dismissAllKeyPreviews();
- cancelAllMessages();
- onDismissMoreKeysPanel();
+ cancelAllOngoingEvents();
mMoreKeysKeyboardCache.clear();
}
@@ -1320,13 +1175,14 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
if (key.altCodeWhileTyping() && key.isEnabled()) {
params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
}
- if (key.mCode == Constants.CODE_SPACE) {
+ final int code = key.getCode();
+ if (code == Constants.CODE_SPACE) {
drawSpacebar(key, canvas, paint);
// Whether space key needs to show the "..." popup hint for special purposes
if (key.isLongPressEnabled() && mHasMultipleEnabledIMEsOrSubtypes) {
drawKeyPopupHint(key, canvas, paint, params);
}
- } else if (key.mCode == Constants.CODE_LANGUAGE_SWITCH) {
+ } else if (code == Constants.CODE_LANGUAGE_SWITCH) {
super.onDrawKeyTopVisuals(key, canvas, paint, params);
drawKeyPopupHint(key, canvas, paint, params);
} else {
@@ -1334,38 +1190,39 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
}
- private static boolean fitsTextIntoWidth(final int width, final String text,
- final Paint paint) {
+ 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);
if (textWidth < width) {
return true;
}
- final float scaleX = width / textWidth;
+ final float scaleX = maxTextWidth / textWidth;
if (scaleX < MINIMUM_XSCALE_OF_LANGUAGE_NAME) {
return false;
}
paint.setTextScaleX(scaleX);
- return TypefaceUtils.getLabelWidth(text, paint) < width;
+ return TypefaceUtils.getLabelWidth(text, paint) < maxTextWidth;
}
// Layout language name on spacebar.
- private static String layoutLanguageOnSpacebar(final Paint paint,
+ private String layoutLanguageOnSpacebar(final Paint paint,
final InputMethodSubtype subtype, final int width) {
+
// Choose appropriate language name to fit into the width.
- final String fullText = getFullDisplayName(subtype);
+ final String fullText = SubtypeLocaleUtils.getFullDisplayName(subtype);
if (fitsTextIntoWidth(width, fullText, paint)) {
return fullText;
}
- final String middleText = getMiddleDisplayName(subtype);
+ final String middleText = SubtypeLocaleUtils.getMiddleDisplayName(subtype);
if (fitsTextIntoWidth(width, middleText, paint)) {
return middleText;
}
- final String shortText = getShortDisplayName(subtype);
+ final String shortText = SubtypeLocaleUtils.getShortDisplayName(subtype);
if (fitsTextIntoWidth(width, shortText, paint)) {
return shortText;
}
@@ -1374,8 +1231,8 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
private void drawSpacebar(final Key key, final Canvas canvas, final Paint paint) {
- final int width = key.mWidth;
- final int height = key.mHeight;
+ final int width = key.getWidth();
+ final int height = key.getHeight();
// If input language are explicitly selected.
if (mNeedsToDisplayLanguage) {
@@ -1412,45 +1269,9 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack
}
}
- // 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.
- static String getFullDisplayName(final InputMethodSubtype subtype) {
- if (SubtypeLocale.isNoLanguage(subtype)) {
- return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
- }
- return SubtypeLocale.getSubtypeLocaleDisplayName(subtype.getLocale());
- }
-
- // Get InputMethodSubtype's short display name in its locale.
- static String getShortDisplayName(final InputMethodSubtype subtype) {
- if (SubtypeLocale.isNoLanguage(subtype)) {
- return "";
- }
- final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
- return StringUtils.capitalizeFirstCodePoint(locale.getLanguage(), locale);
- }
-
- // Get InputMethodSubtype's middle display name in its locale.
- static String getMiddleDisplayName(final InputMethodSubtype subtype) {
- if (SubtypeLocale.isNoLanguage(subtype)) {
- return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
- }
- final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
- return SubtypeLocale.getSubtypeLocaleDisplayName(locale.getLanguage());
+ @Override
+ public void deallocateMemory() {
+ super.deallocateMemory();
+ mGestureTrailsPreview.deallocateMemory();
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
index a2001cb8f..6b76e2461 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
@@ -39,7 +39,7 @@ public final class MoreKeysDetector extends KeyDetector {
Key nearestKey = null;
int nearestDist = (y < 0) ? mSlideAllowanceSquareTop : mSlideAllowanceSquare;
- for (final Key key : getKeyboard().mKeys) {
+ for (final Key key : getKeyboard().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 ae08a5953..8256d4623 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
@@ -17,7 +17,6 @@
package com.android.inputmethod.keyboard;
import android.content.Context;
-import android.content.res.Resources;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
@@ -28,7 +27,8 @@ import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
import com.android.inputmethod.keyboard.internal.KeyboardParams;
import com.android.inputmethod.keyboard.internal.MoreKeySpec;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.StringUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.utils.TypefaceUtils;
public final class MoreKeysKeyboard extends Keyboard {
private final int mDefaultKeyCoordX;
@@ -75,10 +75,8 @@ public final class MoreKeysKeyboard extends Keyboard {
final boolean isFixedColumnOrder, final int dividerWidth) {
mIsFixedOrder = isFixedColumnOrder;
if (parentKeyboardWidth / keyWidth < Math.min(numKeys, maxColumns)) {
- throw new IllegalArgumentException(
- "Keyboard is too small to hold more keys keyboard: "
- + parentKeyboardWidth + " " + keyWidth + " "
- + numKeys + " " + maxColumns);
+ throw new IllegalArgumentException("Keyboard is too small to hold more keys: "
+ + parentKeyboardWidth + " " + keyWidth + " " + numKeys + " " + maxColumns);
}
mDefaultKeyWidth = keyWidth;
mDefaultRowHeight = rowHeight;
@@ -278,9 +276,16 @@ public final class MoreKeysKeyboard extends Keyboard {
mParams.mVerticalGap = parentKeyboard.mVerticalGap / 2;
mParentKey = parentKey;
+ final MoreKeySpec[] moreKeys = parentKey.getMoreKeys();
final int width, height;
+ // {@link KeyPreviewDrawParams#mPreviewVisibleWidth} should have been set at
+ // {@link MainKeyboardView#showKeyPreview(PointerTracker}, though there may be
+ // some chances that the value is zero. <code>width == 0</code> will cause
+ // zero-division error at
+ // {@link MoreKeysKeyboardParams#setParameters(int,int,int,int,int,int,boolean,int)}.
final boolean singleMoreKeyWithPreview = parentKeyboardView.isKeyPreviewPopupEnabled()
- && !parentKey.noKeyPreview() && parentKey.mMoreKeys.length == 1;
+ && !parentKey.noKeyPreview() && moreKeys.length == 1
+ && keyPreviewDrawParams.mPreviewVisibleWidth > 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.
@@ -291,22 +296,14 @@ public final class MoreKeysKeyboard extends Keyboard {
// adjusted with their bottom paddings deducted.
width = keyPreviewDrawParams.mPreviewVisibleWidth;
height = keyPreviewDrawParams.mPreviewVisibleHeight + mParams.mVerticalGap;
- // TODO: Remove this check.
- if (width == 0) {
- throw new IllegalArgumentException(
- "Zero width key detected: " + parentKey + " in " + parentKeyboard.mId);
- }
} else {
- width = getMaxKeyWidth(parentKeyboardView, parentKey, mParams.mDefaultKeyWidth,
- context.getResources());
+ final float padding = context.getResources().getDimension(
+ R.dimen.more_keys_keyboard_key_horizontal_padding)
+ + (parentKey.hasLabelsInMoreKeys()
+ ? mParams.mDefaultKeyWidth * LABEL_PADDING_RATIO : 0.0f);
+ width = getMaxKeyWidth(parentKey, mParams.mDefaultKeyWidth, padding,
+ parentKeyboardView.newLabelPaint(parentKey));
height = parentKeyboard.mMostCommonKeyHeight;
- // TODO: Remove this check.
- if (width == 0) {
- throw new IllegalArgumentException(
- "Zero width calculated: " + parentKey
- + " moreKeys=" + java.util.Arrays.toString(parentKey.mMoreKeys)
- + " in " + parentKeyboard.mId);
- }
}
final int dividerWidth;
if (parentKey.needsDividersInMoreKeys()) {
@@ -316,20 +313,16 @@ public final class MoreKeysKeyboard extends Keyboard {
mDivider = null;
dividerWidth = 0;
}
- mParams.setParameters(parentKey.mMoreKeys.length, parentKey.getMoreKeysColumn(),
- width, height, parentKey.mX + parentKey.mWidth / 2,
- parentKeyboardView.getMeasuredWidth(), parentKey.isFixedColumnOrderMoreKeys(),
+ mParams.setParameters(moreKeys.length, parentKey.getMoreKeysColumn(),
+ width, height, parentKey.getX() + parentKey.getWidth() / 2,
+ parentKeyboard.mId.mWidth, parentKey.isFixedColumnOrderMoreKeys(),
dividerWidth);
}
- private static int getMaxKeyWidth(final KeyboardView view, final Key parentKey,
- final int minKeyWidth, final Resources res) {
- final float padding =
- res.getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding)
- + (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0.0f);
- final Paint paint = view.newLabelPaint(parentKey);
+ private static int getMaxKeyWidth(final Key parentKey, final int minKeyWidth,
+ final float padding, final Paint paint) {
int maxWidth = minKeyWidth;
- for (final MoreKeySpec spec : parentKey.mMoreKeys) {
+ for (final MoreKeySpec spec : parentKey.getMoreKeys()) {
final String label = spec.mLabel;
// If the label is single letter, minKeyWidth is enough to hold the label.
if (label != null && StringUtils.codePointCount(label) > 1) {
@@ -344,7 +337,7 @@ public final class MoreKeysKeyboard extends Keyboard {
public MoreKeysKeyboard build() {
final MoreKeysKeyboardParams params = mParams;
final int moreKeyFlags = mParentKey.getMoreKeyLabelFlags();
- final MoreKeySpec[] moreKeys = mParentKey.mMoreKeys;
+ final MoreKeySpec[] moreKeys = mParentKey.getMoreKeys();
for (int n = 0; n < moreKeys.length; n++) {
final MoreKeySpec moreKeySpec = moreKeys[n];
final int row = n / params.mNumColumns;
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
index a82fb79bd..973128d36 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
@@ -23,8 +23,8 @@ import android.view.MotionEvent;
import android.view.View;
import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.CoordinateUtils;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.CoordinateUtils;
/**
* A view that renders a virtual {@link MoreKeysKeyboard}. It handles rendering of keys and
@@ -34,7 +34,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
private final int[] mCoordinates = CoordinateUtils.newInstance();
protected final KeyDetector mKeyDetector;
- private Controller mController;
+ private Controller mController = EMPTY_CONTROLLER;
protected KeyboardActionListener mListener;
private int mOriginX;
private int mOriginY;
@@ -119,7 +119,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();
+ mController.onCancelMoreKeysPanel(this);
}
}
@@ -127,7 +127,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
public void onUpEvent(final int x, final int y, final int pointerId, final long eventTime) {
if (mCurrentKey != null && mActivePointerId == pointerId) {
updateReleaseKeyGraphics(mCurrentKey);
- onCodeInput(mCurrentKey.mCode, x, y);
+ onCodeInput(mCurrentKey.getCode(), x, y);
mCurrentKey = null;
}
}
@@ -173,9 +173,11 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
}
@Override
- public boolean dismissMoreKeysPanel() {
- if (mController == null) return false;
- return mController.onDismissMoreKeysPanel();
+ public void dismissMoreKeysPanel() {
+ if (!isShowingInParent()) {
+ return;
+ }
+ mController.onDismissMoreKeysPanel(this);
}
@Override
@@ -196,12 +198,6 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
final int x = (int)me.getX(index);
final int y = (int)me.getY(index);
final int pointerId = me.getPointerId(index);
- processMotionEvent(action, x, y, pointerId, eventTime);
- return true;
- }
-
- public void processMotionEvent(final int action, final int x, final int y,
- final int pointerId, final long eventTime) {
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
@@ -215,6 +211,7 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
onMoveEvent(x, y, pointerId, eventTime);
break;
}
+ return true;
}
@Override
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java
index 9c677e5c8..886c6286f 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java
@@ -22,21 +22,32 @@ public interface MoreKeysPanel {
public interface Controller {
/**
* Add the {@link MoreKeysPanel} to the target view.
- * @param panel
+ * @param panel the panel to be shown.
*/
public void onShowMoreKeysPanel(final MoreKeysPanel panel);
/**
* Remove the current {@link MoreKeysPanel} from the target view.
+ * @param panel the panel to be dismissed.
*/
- public boolean onDismissMoreKeysPanel();
+ public void onDismissMoreKeysPanel(final MoreKeysPanel panel);
/**
* 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();
+ public void onCancelMoreKeysPanel(final MoreKeysPanel panel);
}
+ public static final Controller EMPTY_CONTROLLER = new Controller() {
+ @Override
+ public void onShowMoreKeysPanel(final MoreKeysPanel panel) {}
+ @Override
+ public void onDismissMoreKeysPanel(final MoreKeysPanel panel) {}
+ @Override
+ public void onCancelMoreKeysPanel(final MoreKeysPanel panel) {}
+ };
+
/**
* Initializes the layout and event handling of this {@link MoreKeysPanel} and calls the
* controller's onShowMoreKeysPanel to add the panel's container view.
@@ -57,7 +68,7 @@ public interface MoreKeysPanel {
* Dismisses the more keys panel and calls the controller's onDismissMoreKeysPanel to remove
* the panel's container view.
*/
- public boolean dismissMoreKeysPanel();
+ public void dismissMoreKeysPanel();
/**
* Process a move event on the more keys panel.
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index 958aaf569..ee4ac950c 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -16,8 +16,10 @@
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;
@@ -27,13 +29,15 @@ import com.android.inputmethod.keyboard.internal.GestureStroke.GestureStrokePara
import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints;
import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints.GestureStrokePreviewParams;
import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.CoordinateUtils;
import com.android.inputmethod.latin.InputPointers;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.latin.settings.Settings;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.CoordinateUtils;
+import com.android.inputmethod.latin.utils.ResourceUtils;
import com.android.inputmethod.research.ResearchLogger;
import java.util.ArrayList;
@@ -60,7 +64,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
/**
* Get KeyboardActionListener object that is used to register key code and so on.
- * @return the KeyboardActionListner for this PointerTracker
+ * @return the KeyboardActionListner for this PointerTracke
*/
public KeyboardActionListener getKeyboardActionListener();
@@ -84,14 +88,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
public void dismissKeyPreview(PointerTracker tracker);
public void showSlidingKeyInputPreview(PointerTracker tracker);
public void dismissSlidingKeyInputPreview();
- public void showGestureTrail(PointerTracker tracker);
+ public void showGestureTrail(PointerTracker tracker, boolean showsFloatingPreviewText);
}
public interface TimerProxy {
public void startTypingStateTimer(Key typedKey);
public boolean isTypingState();
- public void startKeyRepeatTimer(PointerTracker tracker);
- public void startLongPressTimer(PointerTracker tracker);
+ public void startKeyRepeatTimer(PointerTracker tracker, int repeatCount, int delay);
+ public void startLongPressTimer(PointerTracker tracker, int delay);
public void cancelLongPressTimer();
public void startDoubleTapShiftKeyTimer();
public void cancelDoubleTapShiftKeyTimer();
@@ -107,9 +111,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
@Override
public boolean isTypingState() { return false; }
@Override
- public void startKeyRepeatTimer(PointerTracker tracker) {}
+ public void startKeyRepeatTimer(PointerTracker tracker, int repeatCount, int delay) {}
@Override
- public void startLongPressTimer(PointerTracker tracker) {}
+ public void startLongPressTimer(PointerTracker tracker, int delay) {}
@Override
public void cancelLongPressTimer() {}
@Override
@@ -134,6 +138,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
public final int mTouchNoiseThresholdTime;
public final int mTouchNoiseThresholdDistance;
public final int mSuppressKeyPreviewAfterBatchInputDuration;
+ public final int mKeyRepeatStartTimeout;
+ public final int mKeyRepeatInterval;
+ public final int mLongPressShiftLockTimeout;
public static final PointerTrackerParams DEFAULT = new PointerTrackerParams();
@@ -142,6 +149,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
mTouchNoiseThresholdTime = 0;
mTouchNoiseThresholdDistance = 0;
mSuppressKeyPreviewAfterBatchInputDuration = 0;
+ mKeyRepeatStartTimeout = 0;
+ mKeyRepeatInterval = 0;
+ mLongPressShiftLockTimeout = 0;
}
public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) {
@@ -153,6 +163,12 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0);
mSuppressKeyPreviewAfterBatchInputDuration = mainKeyboardViewAttr.getInt(
R.styleable.MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration, 0);
+ mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0);
+ mKeyRepeatInterval = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_keyRepeatInterval, 0);
+ mLongPressShiftLockTimeout = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_longPressShiftLockTimeout, 0);
}
}
@@ -164,8 +180,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// 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 might be device specific.
- private static final boolean sNeedsProximateBogusDownMoveUpEventHack = true;
+ // 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();
@@ -175,7 +192,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
private DrawingProxy mDrawingProxy;
private TimerProxy mTimerProxy;
private KeyDetector mKeyDetector;
- private KeyboardActionListener mListener = KeyboardActionListener.Adapter.EMPTY_LISTENER;
+ private KeyboardActionListener mListener = KeyboardActionListener.EMPTY_LISTENER;
private Keyboard mKeyboard;
private int mPhantonSuddenMoveThreshold;
@@ -323,19 +340,48 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// the more keys panel currently being shown. equals null if no panel is active.
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,
// so that further modifier keys should be ignored.
boolean mIsInSlidingKeyInputFromModifier;
+ // 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;
- public static void init(final boolean needsPhantomSuddenMoveEventHack) {
- sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack;
+ 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;
+ }
+
+ 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;
@@ -383,6 +429,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
return sPointerTrackerQueue.isAnyInSlidingKeyInput();
}
+ public static void cancelAllPointerTrackers() {
+ sPointerTrackerQueue.cancelAllPointerTrackers();
+ }
+
public static void setKeyboardActionListener(final KeyboardActionListener listener) {
final int trackersSize = sTrackers.size();
for (int i = 0; i < trackersSize; ++i) {
@@ -430,6 +480,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
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();
@@ -437,7 +491,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
}
// Returns true if keyboard has been changed by this callback.
- private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) {
+ private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key,
+ final int repeatCount) {
// While gesture input is going on, this method should be a no-operation. But when gesture
// input has been canceled, <code>sInGesture</code> and <code>mIsDetectingGesture</code>
// are set to false. To keep this method is a no-operation,
@@ -447,16 +502,17 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
}
final boolean ignoreModifierKey = mIsInSlidingKeyInput && key.isModifier();
if (DEBUG_LISTENER) {
- Log.d(TAG, String.format("[%d] onPress : %s%s%s", mPointerId,
+ Log.d(TAG, String.format("[%d] onPress : %s%s%s%s", mPointerId,
KeyDetector.printableCode(key),
ignoreModifierKey ? " ignoreModifier" : "",
- key.isEnabled() ? "" : " disabled"));
+ key.isEnabled() ? "" : " disabled",
+ repeatCount > 0 ? " repeatCount=" + repeatCount : ""));
}
if (ignoreModifierKey) {
return false;
}
if (key.isEnabled()) {
- mListener.onPressKey(key.mCode, getActivePointerTrackerCount() == 1);
+ mListener.onPressKey(key.getCode(), repeatCount, getActivePointerTrackerCount() == 1);
final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
mKeyboardLayoutHasBeenChanged = false;
mTimerProxy.startTypingStateTimer(key);
@@ -567,10 +623,6 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
return mIsInSlidingKeyInput;
}
- public boolean isInSlidingKeyInputFromModifier() {
- return mIsInSlidingKeyInputFromModifier;
- }
-
public Key getKey() {
return mCurrentKey;
}
@@ -718,7 +770,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
return sPointerTrackerQueue.size();
}
- public boolean isOldestTrackerInQueue() {
+ private boolean isOldestTrackerInQueue() {
return sPointerTrackerQueue.getOldestElement() == this;
}
@@ -726,7 +778,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (sInGesture || !mGestureStrokeWithPreviewPoints.isStartOfAGesture()) {
return;
}
- if (key == null || !Character.isLetter(key.mCode)) {
+ if (key == null || !Character.isLetter(key.getCode())) {
return;
}
if (DEBUG_LISTENER) {
@@ -741,7 +793,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
dismissAllMoreKeysPanels();
}
mTimerProxy.cancelLongPressTimer();
- mDrawingProxy.showGestureTrail(this);
+ // 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) {
@@ -757,7 +811,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (mIsTrackingForActionDisabled) {
return;
}
- mDrawingProxy.showGestureTrail(this);
+ // A gesture floating preview text will be shown at the oldest pointer/finger on the screen.
+ mDrawingProxy.showGestureTrail(
+ this, isOldestTrackerInQueue() /* showsFloatingPreviewText */);
}
private void updateBatchInput(final long eventTime) {
@@ -798,11 +854,13 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (mIsTrackingForActionDisabled) {
return;
}
- mDrawingProxy.showGestureTrail(this);
+ // A gesture floating preview text will be shown at the oldest pointer/finger on the screen.
+ mDrawingProxy.showGestureTrail(
+ this, isOldestTrackerInQueue() /* showsFloatingPreviewText */);
}
private void cancelBatchInput() {
- sPointerTrackerQueue.cancelAllPointerTracker();
+ cancelAllPointerTrackers();
mIsDetectingGesture = false;
if (!sInGesture) {
return;
@@ -814,8 +872,23 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
mListener.onCancelBatchInput();
}
- public void processMotionEvent(final int action, final int x, final int y, final long eventTime,
- final KeyEventHandler handler) {
+ public void processMotionEvent(final MotionEvent me, final KeyEventHandler handler) {
+ final int action = me.getActionMasked();
+ final long eventTime = me.getEventTime();
+ if (action == MotionEvent.ACTION_MOVE) {
+ final int pointerCount = me.getPointerCount();
+ for (int index = 0; index < pointerCount; index++) {
+ final int id = me.getPointerId(index);
+ final PointerTracker tracker = getPointerTracker(id, handler);
+ final int x = (int)me.getX(index);
+ final int y = (int)me.getY(index);
+ tracker.onMoveEvent(x, y, eventTime, me);
+ }
+ return;
+ }
+ final int index = me.getActionIndex();
+ final int x = (int)me.getX(index);
+ final int y = (int)me.getY(index);
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
@@ -825,24 +898,18 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
case MotionEvent.ACTION_POINTER_UP:
onUpEvent(x, y, eventTime);
break;
- case MotionEvent.ACTION_MOVE:
- onMoveEvent(x, y, eventTime, null);
- break;
case MotionEvent.ACTION_CANCEL:
onCancelEvent(x, y, eventTime);
break;
}
}
- public void onDownEvent(final int x, final int y, final long eventTime,
+ private void onDownEvent(final int x, final int y, final long eventTime,
final KeyEventHandler handler) {
if (DEBUG_EVENT) {
printTouchEvent("onDownEvent:", x, y, eventTime);
}
- mDrawingProxy = handler.getDrawingProxy();
- mTimerProxy = handler.getTimerProxy();
- setKeyboardActionListener(handler.getKeyboardActionListener());
- setKeyDetectorInner(handler.getKeyDetector());
+ setKeyEventHandler(handler);
// Naive up-to-down noise filter.
final long deltaT = eventTime - mUpTime;
if (deltaT < sParams.mTouchNoiseThresholdTime) {
@@ -872,7 +939,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (!sShouldHandleGesture) {
return;
}
- // A gesture should start only from a non-modifier key.
+ // A gesture should start only from a non-modifier key. Note that the gesture detection is
+ // disabled when the key is repeating.
mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard()
&& key != null && !key.isModifier();
if (mIsDetectingGesture) {
@@ -902,7 +970,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// 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
// keyboard layout.
- if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
+ if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
key = onDownKey(x, y, eventTime);
}
@@ -952,7 +1020,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
}
}
- public void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) {
+ private void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) {
if (DEBUG_MOVE_EVENT) {
printTouchEvent("onMoveEvent:", x, y, eventTime);
}
@@ -978,7 +1046,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
final int translatedY = mMoreKeysPanel.translateY(y);
mMoreKeysPanel.onMoveEvent(translatedX, translatedY, mPointerId, eventTime);
onMoveKey(x, y);
- mDrawingProxy.showSlidingKeyInputPreview(this);
+ if (mIsInSlidingKeyInputFromModifier) {
+ mDrawingProxy.showSlidingKeyInputPreview(this);
+ }
return;
}
onMoveEventInternal(x, y, eventTime);
@@ -990,7 +1060,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// at {@link #setKeyboard}. In those cases, we should update key according
// to the new keyboard layout.
Key key = newKey;
- if (callListenerOnPressAndCheckKeyboardLayoutChange(key)) {
+ if (callListenerOnPressAndCheckKeyboardLayoutChange(key, 0 /* repeatCount */)) {
key = onMoveKey(x, y);
}
onMoveToNewKey(key, x, y);
@@ -1008,8 +1078,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
+ " phantom sudden move event (distance=%d) is translated to "
+ "up[%d,%d,%s]/down[%d,%d,%s] events", mPointerId,
getDistance(x, y, lastX, lastY),
- lastX, lastY, Constants.printableCode(oldKey.mCode),
- x, y, Constants.printableCode(key.mCode)));
+ lastX, lastY, Constants.printableCode(oldKey.getCode()),
+ x, y, Constants.printableCode(key.getCode())));
}
// TODO: This should be moved to outside of this nested if-clause?
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -1031,8 +1101,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
+ " bogus down-move-up event (raidus=%.2f key diagonal) is "
+ " translated to up[%d,%d,%s]/down[%d,%d,%s] events",
mPointerId, radiusRatio,
- lastX, lastY, Constants.printableCode(oldKey.mCode),
- x, y, Constants.printableCode(key.mCode)));
+ lastX, lastY, Constants.printableCode(oldKey.getCode()),
+ x, y, Constants.printableCode(key.getCode())));
}
onUpEventInternal(x, y, eventTime);
onDownEventInternal(x, y, eventTime);
@@ -1040,7 +1110,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
private void processSildeOutFromOldKey(final Key oldKey) {
setReleasedKeyGraphics(oldKey);
- callListenerOnRelease(oldKey, oldKey.mCode, true /* withSliding */);
+ callListenerOnRelease(oldKey, oldKey.getCode(), true /* withSliding */);
startSlidingKeyInput(oldKey);
mTimerProxy.cancelKeyTimers();
}
@@ -1133,10 +1203,12 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
slideOutFromOldKey(oldKey, x, y);
}
}
- mDrawingProxy.showSlidingKeyInputPreview(this);
+ if (mIsInSlidingKeyInputFromModifier) {
+ mDrawingProxy.showSlidingKeyInputPreview(this);
+ }
}
- public void onUpEvent(final int x, final int y, final long eventTime) {
+ private void onUpEvent(final int x, final int y, final long eventTime) {
if (DEBUG_EVENT) {
printTouchEvent("onUpEvent :", x, y, eventTime);
}
@@ -1178,6 +1250,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
mIsDetectingGesture = false;
final Key currentKey = mCurrentKey;
mCurrentKey = null;
+ final int currentRepeatingKeyCode = mCurrentRepeatingKeyCode;
+ mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
// Release the last pressed key.
setReleasedKeyGraphics(currentKey);
@@ -1194,7 +1268,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (sInGesture) {
if (currentKey != null) {
- callListenerOnRelease(currentKey, currentKey.mCode, true /* withSliding */);
+ callListenerOnRelease(currentKey, currentKey.getCode(), true /* withSliding */);
}
mayEndBatchInput(eventTime);
return;
@@ -1203,8 +1277,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
if (mIsTrackingForActionDisabled) {
return;
}
- if (currentKey != null && currentKey.isRepeatable() && !isInSlidingKeyInput) {
- // Repeatable key has been registered in {@link #onDownEventInternal(int,int,long)}.
+ if (currentKey != null && currentKey.isRepeatable()
+ && (currentKey.getCode() == currentRepeatingKeyCode) && !isInSlidingKeyInput) {
return;
}
detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
@@ -1213,12 +1287,12 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
}
}
- public void onShowMoreKeysPanel(final int translatedX, final int translatedY,
- final MoreKeysPanel panel) {
+ public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
setReleasedKeyGraphics(mCurrentKey);
- final long eventTime = SystemClock.uptimeMillis();
+ final int translatedX = panel.translateX(mLastX);
+ final int translatedY = panel.translateY(mLastY);
+ panel.onDownEvent(translatedX, translatedY, mPointerId, SystemClock.uptimeMillis());
mMoreKeysPanel = panel;
- mMoreKeysPanel.onDownEvent(translatedX, translatedY, mPointerId, eventTime);
}
@Override
@@ -1236,13 +1310,13 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
sPointerTrackerQueue.remove(this);
}
- public void onCancelEvent(final int x, final int y, final long eventTime) {
+ private void onCancelEvent(final int x, final int y, final long eventTime) {
if (DEBUG_EVENT) {
printTouchEvent("onCancelEvt:", x, y, eventTime);
}
cancelBatchInput();
- sPointerTrackerQueue.cancelAllPointerTracker();
+ cancelAllPointerTrackers();
sPointerTrackerQueue.releaseAllPointers(eventTime);
onCancelEventInternal();
}
@@ -1257,21 +1331,6 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
}
}
- private void startRepeatKey(final Key key) {
- 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;
- onRepeatKey(key);
- mTimerProxy.startKeyRepeatTimer(this);
- }
-
- public void onRepeatKey(final Key key) {
- detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
- mTimerProxy.startTypingStateTimer(key);
- }
-
private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime,
final Key newKey) {
if (mKeyDetector == null) {
@@ -1322,8 +1381,23 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
// doesn't have its more keys. (e.g. spacebar, globe 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.mMoreKeys == null) return;
- mTimerProxy.startLongPressTimer(this);
+ 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;
+ }
+ mTimerProxy.startLongPressTimer(this, delay);
}
private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) {
@@ -1332,11 +1406,35 @@ public final class PointerTracker implements PointerTrackerQueue.Element {
return;
}
- final int code = key.mCode;
+ final int code = key.getCode();
callListenerOnCodeInput(key, code, x, y, eventTime);
callListenerOnRelease(key, code, false /* withSliding */);
}
+ private void startRepeatKey(final Key key) {
+ 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;
+ final int startRepeatCount = 1;
+ mTimerProxy.startKeyRepeatTimer(this, startRepeatCount, sParams.mKeyRepeatStartTimeout);
+ }
+
+ public void onKeyRepeat(final int code, final int repeatCount) {
+ final Key key = getKey();
+ if (key == null || key.getCode() != code) {
+ mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
+ return;
+ }
+ mCurrentRepeatingKeyCode = code;
+ mIsDetectingGesture = false;
+ final int nextRepeatCount = repeatCount + 1;
+ mTimerProxy.startKeyRepeatTimer(this, nextRepeatCount, sParams.mKeyRepeatInterval);
+ callListenerOnPressAndCheckKeyboardLayoutChange(key, repeatCount);
+ callListenerOnCodeInput(key, code, mKeyX, mKeyY, SystemClock.uptimeMillis());
+ }
+
private void printTouchEvent(final String title, final int x, final int y,
final long eventTime) {
final Key key = mKeyDetector.detectHitKey(x, y);
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index 57d3fede4..a0316696c 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -22,7 +22,7 @@ import android.util.Log;
import com.android.inputmethod.keyboard.internal.TouchPositionCorrection;
import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.JniUtils;
+import com.android.inputmethod.latin.utils.JniUtils;
import java.util.Arrays;
@@ -96,7 +96,7 @@ public class ProximityInfo {
private static boolean needsProximityInfo(final Key key) {
// Don't include special keys into ProximityInfo.
- return key.mCode >= Constants.CODE_SPACE;
+ return key.getCode() >= Constants.CODE_SPACE;
}
private static int getProximityInfoKeysCount(final Key[] keys) {
@@ -122,7 +122,7 @@ public class ProximityInfo {
if (!needsProximityInfo(neighborKey)) {
continue;
}
- proximityCharsArray[infoIndex] = neighborKey.mCode;
+ proximityCharsArray[infoIndex] = neighborKey.getCode();
infoIndex++;
}
}
@@ -159,11 +159,11 @@ public class ProximityInfo {
if (!needsProximityInfo(key)) {
continue;
}
- keyXCoordinates[infoIndex] = key.mX;
- keyYCoordinates[infoIndex] = key.mY;
- keyWidths[infoIndex] = key.mWidth;
- keyHeights[infoIndex] = key.mHeight;
- keyCharCodes[infoIndex] = key.mCode;
+ keyXCoordinates[infoIndex] = key.getX();
+ keyYCoordinates[infoIndex] = key.getY();
+ keyWidths[infoIndex] = key.getWidth();
+ keyHeights[infoIndex] = key.getHeight();
+ keyCharCodes[infoIndex] = key.getCode();
infoIndex++;
}
@@ -183,7 +183,7 @@ public class ProximityInfo {
if (!needsProximityInfo(key)) {
continue;
}
- final Rect hitBox = key.mHitBox;
+ final Rect hitBox = key.getHitBox();
sweetSpotCenterXs[infoIndex] = hitBox.exactCenterX();
sweetSpotCenterYs[infoIndex] = hitBox.exactCenterY();
sweetSpotRadii[infoIndex] = defaultRadius;
@@ -204,7 +204,7 @@ public class ProximityInfo {
" [%2d] row=%d x/y/r=%7.2f/%7.2f/%5.2f %s code=%s", infoIndex, row,
sweetSpotCenterXs[infoIndex], sweetSpotCenterYs[infoIndex],
sweetSpotRadii[infoIndex], (row < rows ? "correct" : "default"),
- Constants.printableCode(key.mCode)));
+ Constants.printableCode(key.getCode())));
}
infoIndex++;
}
@@ -240,28 +240,123 @@ public class ProximityInfo {
private void computeNearestNeighbors() {
final int defaultWidth = mMostCommonKeyWidth;
- final Key[] keys = mKeys;
- final int thresholdBase = (int) (defaultWidth * SEARCH_DISTANCE);
- final int threshold = thresholdBase * thresholdBase;
+ final int keyCount = mKeys.length;
+ final int gridSize = mGridNeighbors.length;
+ final int threshold = (int) (defaultWidth * SEARCH_DISTANCE);
+ final int thresholdSquared = threshold * threshold;
// Round-up so we don't have any pixels outside the grid
- final Key[] neighborKeys = new Key[keys.length];
- final int gridWidth = mGridWidth * mCellWidth;
- final int gridHeight = mGridHeight * mCellHeight;
- for (int x = 0; x < gridWidth; x += mCellWidth) {
- for (int y = 0; y < gridHeight; y += mCellHeight) {
- final int centerX = x + mCellWidth / 2;
- final int centerY = y + mCellHeight / 2;
- int count = 0;
- for (final Key key : keys) {
- if (key.isSpacer()) continue;
- if (key.squaredDistanceToEdge(centerX, centerY) < threshold) {
- neighborKeys[count++] = key;
+ final int lastPixelXCoordinate = mGridWidth * mCellWidth - 1;
+ final int lastPixelYCoordinate = mGridHeight * mCellHeight - 1;
+
+ // For large layouts, 'neighborsFlatBuffer' is about 80k of memory: gridSize is usually 512,
+ // keycount is about 40 and a pointer to a Key is 4 bytes. This contains, for each cell,
+ // enough space for as many keys as there are on the keyboard. Hence, every
+ // keycount'th element is the start of a new cell, and each of these virtual subarrays
+ // start empty with keycount spaces available. This fills up gradually in the loop below.
+ // Since in the practice each cell does not have a lot of neighbors, most of this space is
+ // actually just empty padding in this fixed-size buffer.
+ final Key[] neighborsFlatBuffer = new Key[gridSize * keyCount];
+ final int[] neighborCountPerCell = new int[gridSize];
+ final int halfCellWidth = mCellWidth / 2;
+ final int halfCellHeight = mCellHeight / 2;
+ for (final Key key : mKeys) {
+ if (key.isSpacer()) continue;
+
+/* HOW WE PRE-SELECT THE CELLS (iterate over only the relevant cells, instead of all of them)
+
+ We want to compute the distance for keys that are in the cells that are close enough to the
+ key border, as this method is performance-critical. These keys are represented with 'star'
+ background on the diagram below. Let's consider the Y case first.
+
+ We want to select the cells which center falls between the top of the key minus the threshold,
+ and the bottom of the key plus the threshold.
+ topPixelWithinThreshold is key.mY - threshold, and bottomPixelWithinThreshold is
+ key.mY + key.mHeight + threshold.
+
+ Then we need to compute the center of the top row that we need to evaluate, as we'll iterate
+ from there.
+
+(0,0)----> x
+| .-------------------------------------------.
+| | | | | | | | | | | | |
+| |---+---+---+---+---+---+---+---+---+---+---| .- top of top cell (aligned on the grid)
+| | | | | | | | | | | | | |
+| |-----------+---+---+---+---+---+---+---+---|---' v
+| | | | |***|***|*_________________________ topPixelWithinThreshold | yDeltaToGrid
+| |---+---+---+-----^-+-|-+---+---+---+---+---| ^
+| | | | |***|*|*|*|*|***|***| | | | ______________________________________
+v |---+---+--threshold--|-+---+---+---+---+---| |
+ | | | |***|*|*|*|*|***|***| | | | | Starting from key.mY, we substract
+y |---+---+---+---+-v-+-|-+---+---+---+---+---| | thresholdBase and get the top pixel
+ | | | |***|**########------------------- key.mY | within the threshold. We align that on
+ |---+---+---+---+--#+---+-#-+---+---+---+---| | the grid by computing the delta to the
+ | | | |***|**#|***|*#*|***| | | | | grid, and get the top of the top cell.
+ |---+---+---+---+--#+---+-#-+---+---+---+---| |
+ | | | |***|**########*|***| | | | | Adding half the cell height to the top
+ |---+---+---+---+---+-|-+---+---+---+---+---| | of the top cell, we get the middle of
+ | | | |***|***|*|*|***|***| | | | | the top cell (yMiddleOfTopCell).
+ |---+---+---+---+---+-|-+---+---+---+---+---| |
+ | | | |***|***|*|*|***|***| | | | |
+ |---+---+---+---+---+-|________________________ yEnd | Since we only want to add the key to
+ | | | | | | | (bottomPixelWithinThreshold) | the proximity if it's close enough to
+ |---+---+---+---+---+---+---+---+---+---+---| | the center of the cell, we only need
+ | | | | | | | | | | | | | to compute for these cells where
+ '---'---'---'---'---'---'---'---'---'---'---' | topPixelWithinThreshold is above the
+ (positive x,y) | center of the cell. This is the case
+ | when yDeltaToGrid is less than half
+ [Zoomed in diagram] | the height of the cell.
+ +-------+-------+-------+-------+-------+ |
+ | | | | | | | On the zoomed in diagram, on the right
+ | | | | | | | the topPixelWithinThreshold (represented
+ | | | | | | top of | with an = sign) is below and we can skip
+ +-------+-------+-------+--v----+-------+ .. top cell | this cell, while on the left it's above
+ | | = topPixelWT | | yDeltaToGrid | and we need to compute for this cell.
+ |..yStart.|.....|.......|..|....|.......|... y middle | Thus, if yDeltaToGrid is more than half
+ | (left)| | | ^ = | | of top cell | the height of the cell, we start the
+ +-------+-|-----+-------+----|--+-------+ | iteration one cell below the top cell,
+ | | | | | | | | | else we start it on the top cell. This
+ |.......|.|.....|.......|....|..|.....yStart (right) | is stored in yStart.
+
+ Since we only want to go up to bottomPixelWithinThreshold, and we only iterate on the center
+ of the keys, we can stop as soon as the y value exceeds bottomPixelThreshold, so we don't
+ have to align this on the center of the key. Hence, we don't need a separate value for
+ bottomPixelWithinThreshold and call this yEnd right away.
+*/
+ final int keyX = key.getX();
+ final int keyY = key.getY();
+ final int topPixelWithinThreshold = keyY - threshold;
+ final int yDeltaToGrid = topPixelWithinThreshold % mCellHeight;
+ final int yMiddleOfTopCell = topPixelWithinThreshold - yDeltaToGrid + halfCellHeight;
+ final int yStart = Math.max(halfCellHeight,
+ yMiddleOfTopCell + (yDeltaToGrid <= halfCellHeight ? 0 : mCellHeight));
+ final int yEnd = Math.min(lastPixelYCoordinate, keyY + key.getHeight() + threshold);
+
+ final int leftPixelWithinThreshold = keyX - threshold;
+ final int xDeltaToGrid = leftPixelWithinThreshold % mCellWidth;
+ final int xMiddleOfLeftCell = leftPixelWithinThreshold - xDeltaToGrid + halfCellWidth;
+ final int xStart = Math.max(halfCellWidth,
+ xMiddleOfLeftCell + (xDeltaToGrid <= halfCellWidth ? 0 : mCellWidth));
+ final int xEnd = Math.min(lastPixelXCoordinate, keyX + key.getWidth() + threshold);
+
+ int baseIndexOfCurrentRow = (yStart / mCellHeight) * mGridWidth + (xStart / mCellWidth);
+ for (int centerY = yStart; centerY <= yEnd; centerY += mCellHeight) {
+ int index = baseIndexOfCurrentRow;
+ for (int centerX = xStart; centerX <= xEnd; centerX += mCellWidth) {
+ if (key.squaredDistanceToEdge(centerX, centerY) < thresholdSquared) {
+ neighborsFlatBuffer[index * keyCount + neighborCountPerCell[index]] = key;
+ ++neighborCountPerCell[index];
}
+ ++index;
}
- mGridNeighbors[(y / mCellHeight) * mGridWidth + (x / mCellWidth)] =
- Arrays.copyOfRange(neighborKeys, 0, count);
+ baseIndexOfCurrentRow += mGridWidth;
}
}
+
+ for (int i = 0; i < gridSize; ++i) {
+ final int base = i * keyCount;
+ mGridNeighbors[i] =
+ Arrays.copyOfRange(neighborsFlatBuffer, base, base + neighborCountPerCell[i]);
+ }
}
public void fillArrayWithNearestKeyCodes(final int x, final int y, final int primaryKeyCode,
@@ -279,7 +374,7 @@ public class ProximityInfo {
if (index >= destLength) {
break;
}
- final int code = key.mCode;
+ final int code = key.getCode();
if (code <= Constants.CODE_SPACE) {
break;
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
new file mode 100644
index 000000000..c10fdbace
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+/**
+ * The string parser of codesArray specification for <GridRows />. The attribute codesArray is an
+ * array of string.
+ * Each element of the array defines a key label by specifying a code point as a hexadecimal string.
+ * A key label may consist of multiple code points separated by comma.
+ * Each element of the array optionally can have an output text definition after vertical bar
+ * 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]*)?
+ * </pre>
+ */
+// TODO: Write unit tests for this class.
+public final class CodesArrayParser {
+ // Constants for parsing.
+ private static final char COMMA = ',';
+ private static final char VERTICAL_BAR = '|';
+ private static final String COMMA_STRING = ",";
+ private static final int BASE_HEX = 16;
+
+ private CodesArrayParser() {
+ // This utility class is not publicly instantiable.
+ }
+
+ private static String getLabelSpec(final String codesArraySpec) {
+ final int pos = codesArraySpec.indexOf(VERTICAL_BAR);
+ return (pos < 0) ? codesArraySpec : codesArraySpec.substring(0, pos);
+ }
+
+ 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)) {
+ final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
+ sb.appendCodePoint(codePoint);
+ }
+ return sb.toString();
+ }
+
+ private static String getCodeSpec(final String codesArraySpec) {
+ final int pos = codesArraySpec.indexOf(VERTICAL_BAR);
+ return (pos < 0) ? codesArraySpec : codesArraySpec.substring(pos + 1);
+ }
+
+ public static int parseCode(final String codesArraySpec) {
+ final String codeSpec = getCodeSpec(codesArraySpec);
+ if (codeSpec.indexOf(COMMA) < 0) {
+ return Integer.parseInt(codeSpec, BASE_HEX);
+ }
+ return Constants.CODE_OUTPUT_TEXT;
+ }
+
+ public static String parseOutputText(final String codesArraySpec) {
+ final String codeSpec = getCodeSpec(codesArraySpec);
+ if (codeSpec.indexOf(COMMA) < 0) {
+ return null;
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (final String codeInHex : codeSpec.split(COMMA_STRING)) {
+ final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
+ sb.appendCodePoint(codePoint);
+ }
+ return sb.toString();
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
new file mode 100644
index 000000000..0dd71e2ec
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/DynamicGridKeyboard.java
@@ -0,0 +1,223 @@
+/*
+ * 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.SharedPreferences;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.inputmethod.keyboard.EmojiKeyboardView;
+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 java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * This is a Keyboard class where you can add keys dynamically shown in a grid layout
+ */
+public class DynamicGridKeyboard extends Keyboard {
+ private static final String TAG = DynamicGridKeyboard.class.getSimpleName();
+ private static final int TEMPLATE_KEY_CODE_0 = 0x30;
+ private static final int TEMPLATE_KEY_CODE_1 = 0x31;
+
+ private final SharedPreferences mPrefs;
+ private final int mLeftPadding;
+ private final int mHorizontalStep;
+ private final int mVerticalStep;
+ private final int mColumnsNum;
+ private final int mMaxKeyCount;
+ private final boolean mIsRecents;
+ private final ArrayDeque<GridKey> mGridKeys = CollectionUtils.newArrayDeque();
+
+ private Key[] mCachedGridKeys;
+
+ public DynamicGridKeyboard(final SharedPreferences prefs, final Keyboard templateKeyboard,
+ final int maxKeyCount, final int categoryId, final int categoryPageId) {
+ super(templateKeyboard);
+ final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0);
+ final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1);
+ mLeftPadding = key0.getX();
+ mHorizontalStep = Math.abs(key1.getX() - key0.getX());
+ mVerticalStep = key0.getHeight() + mVerticalGap;
+ mColumnsNum = mBaseWidth / mHorizontalStep;
+ mMaxKeyCount = maxKeyCount;
+ mIsRecents = categoryId == EmojiKeyboardView.CATEGORY_ID_RECENTS;
+ mPrefs = prefs;
+ }
+
+ private Key getTemplateKey(final int code) {
+ for (final Key key : super.getKeys()) {
+ if (key.getCode() == code) {
+ return key;
+ }
+ }
+ throw new RuntimeException("Can't find template key: code=" + code);
+ }
+
+ public void addKeyFirst(final Key usedKey) {
+ addKey(usedKey, true);
+ if (mIsRecents) {
+ saveRecentKeys();
+ }
+ }
+
+ public void addKeyLast(final Key usedKey) {
+ addKey(usedKey, false);
+ }
+
+ private void addKey(final Key usedKey, final boolean addFirst) {
+ if (usedKey == null) {
+ return;
+ }
+ synchronized (mGridKeys) {
+ mCachedGridKeys = null;
+ final GridKey key = new GridKey(usedKey);
+ while (mGridKeys.remove(key)) {
+ // Remove duplicate keys.
+ }
+ if (addFirst) {
+ mGridKeys.addFirst(key);
+ } else {
+ mGridKeys.addLast(key);
+ }
+ while (mGridKeys.size() > mMaxKeyCount) {
+ mGridKeys.removeLast();
+ }
+ int index = 0;
+ for (final GridKey gridKey : mGridKeys) {
+ final int keyX = getKeyX(index);
+ final int keyY = getKeyY(index);
+ gridKey.updateCorrdinates(keyX, keyY);
+ index++;
+ }
+ }
+ }
+
+ private void saveRecentKeys() {
+ final ArrayList<Object> keys = CollectionUtils.newArrayList();
+ for (final Key key : mGridKeys) {
+ if (key.getOutputText() != null) {
+ keys.add(key.getOutputText());
+ } else {
+ keys.add(key.getCode());
+ }
+ }
+ final String jsonStr = StringUtils.listToJsonStr(keys);
+ Settings.writeEmojiRecentKeys(mPrefs, jsonStr);
+ }
+
+ private static Key getKey(final Collection<DynamicGridKeyboard> keyboards, final Object o) {
+ 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);
+ }
+ }
+ return null;
+ }
+
+ public void loadRecentKeys(Collection<DynamicGridKeyboard> keyboards) {
+ final String str = Settings.readEmojiRecentKeys(mPrefs);
+ final List<Object> keys = StringUtils.jsonStrToList(str);
+ for (final Object o : keys) {
+ addKeyLast(getKey(keyboards, o));
+ }
+ }
+
+ private int getKeyX(final int index) {
+ final int column = index % mColumnsNum;
+ return column * mHorizontalStep + mLeftPadding;
+ }
+
+ private int getKeyY(final int index) {
+ final int row = index / mColumnsNum;
+ return row * mVerticalStep + mTopPadding;
+ }
+
+ @Override
+ public Key[] getKeys() {
+ synchronized (mGridKeys) {
+ if (mCachedGridKeys != null) {
+ return mCachedGridKeys;
+ }
+ mCachedGridKeys = mGridKeys.toArray(new Key[mGridKeys.size()]);
+ return mCachedGridKeys;
+ }
+ }
+
+ @Override
+ public Key[] getNearestKeys(final int x, final int y) {
+ // TODO: Calculate the nearest key index in mGridKeys from x and y.
+ return getKeys();
+ }
+
+ static final class GridKey extends Key {
+ private int mCurrentX;
+ private int mCurrentY;
+
+ public GridKey(final Key originalKey) {
+ super(originalKey);
+ }
+
+ public void updateCorrdinates(final int x, final int y) {
+ mCurrentX = x;
+ mCurrentY = y;
+ getHitBox().set(x, y, x + getWidth(), y + getHeight());
+ }
+
+ @Override
+ public int getX() {
+ return mCurrentX;
+ }
+
+ @Override
+ public int getY() {
+ return mCurrentY;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (!(o instanceof Key)) return false;
+ final Key key = (Key)o;
+ if (getCode() != key.getCode()) return false;
+ if (!TextUtils.equals(getLabel(), key.getLabel())) return false;
+ return TextUtils.equals(getOutputText(), key.getOutputText());
+ }
+
+ @Override
+ public String toString() {
+ return "GridKey: " + super.toString();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java
index ab810a580..c6dd9e100 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingPreviewText.java
@@ -26,9 +26,9 @@ import android.text.TextUtils;
import android.view.View;
import com.android.inputmethod.keyboard.PointerTracker;
-import com.android.inputmethod.latin.CoordinateUtils;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.utils.CoordinateUtils;
/**
* The class for single gesture preview text. The class for multiple gesture preview text will be
@@ -115,9 +115,7 @@ public class GestureFloatingPreviewText extends AbstractDrawingPreview {
@Override
public void setPreviewPosition(final PointerTracker tracker) {
- final boolean needsToUpdateLastPointer =
- tracker.isOldestTrackerInQueue() && isPreviewEnabled();
- if (!needsToUpdateLastPointer) {
+ if (!isPreviewEnabled()) {
return;
}
tracker.getLastCoordinates(mLastPointerCoords);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
index 70363e602..f29ade861 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
@@ -21,8 +21,8 @@ import android.util.Log;
import com.android.inputmethod.latin.InputPointers;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResizableIntArray;
-import com.android.inputmethod.latin.ResourceUtils;
+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();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
index 8deadbf96..ecc67dd44 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
@@ -19,7 +19,7 @@ package com.android.inputmethod.keyboard.internal;
import android.content.res.TypedArray;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResizableIntArray;
+import com.android.inputmethod.latin.utils.ResizableIntArray;
public final class GestureStrokeWithPreviewPoints extends GestureStroke {
public static final int PREVIEW_CAPACITY = 256;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java
index 0f3cd7887..aca667919 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java
@@ -26,7 +26,7 @@ import android.os.SystemClock;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResizableIntArray;
+import com.android.inputmethod.latin.utils.ResizableIntArray;
/*
* @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutStartDelay
@@ -245,7 +245,7 @@ final class GestureTrail {
final float body1 = r1 * params.mTrailBodyRatio;
final float body2 = r2 * params.mTrailBodyRatio;
final Path path = roundedLine.makePath(p1x, p1y, body1, p2x, p2y, body2);
- if (path != null) {
+ if (!path.isEmpty()) {
roundedLine.getBounds(mRoundedLineBounds);
if (params.mTrailShadowEnabled) {
final float shadow2 = r2 * params.mTrailShadowRatio;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java
index 1e4c43ee5..19e995548 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java
@@ -30,8 +30,8 @@ import android.view.View;
import com.android.inputmethod.keyboard.PointerTracker;
import com.android.inputmethod.keyboard.internal.GestureTrail.Params;
-import com.android.inputmethod.latin.CollectionUtils;
-import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
/**
* Draw gesture trail preview graphics during gesture.
@@ -104,7 +104,13 @@ public final class GestureTrailsPreview extends AbstractDrawingPreview {
freeOffscreenBuffer();
}
+ public void deallocateMemory() {
+ freeOffscreenBuffer();
+ }
+
private void freeOffscreenBuffer() {
+ mOffscreenCanvas.setBitmap(null);
+ mOffscreenCanvas.setMatrix(null);
if (mOffscreenBuffer != null) {
mOffscreenBuffer.recycle();
mOffscreenBuffer = null;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java
index 5dcd842f7..1716fa049 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java
@@ -18,7 +18,7 @@ package com.android.inputmethod.keyboard.internal;
import android.graphics.Typeface;
-import com.android.inputmethod.latin.ResourceUtils;
+import com.android.inputmethod.latin.utils.ResourceUtils;
public final class KeyDrawParams {
public Typeface mTypeface;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
index ba449eeb3..22f5b3dd1 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -21,10 +21,10 @@ import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED;
import android.text.TextUtils;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.StringUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
index f65056948..e6a674334 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -24,7 +24,7 @@ public abstract class KeyStyle {
public abstract String[] getStringArray(TypedArray a, int index);
public abstract String getString(TypedArray a, int index);
public abstract int getInt(TypedArray a, int index, int defaultValue);
- public abstract int getFlag(TypedArray a, int index);
+ public abstract int getFlags(TypedArray a, int index);
protected KeyStyle(final KeyboardTextsSet textsSet) {
mTextsSet = textsSet;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
index a048ad09f..05d855e31 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
@@ -20,9 +20,9 @@ import android.content.res.TypedArray;
import android.util.Log;
import android.util.SparseArray;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.XmlParseUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.XmlParseUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -66,7 +66,7 @@ public final class KeyStylesSet {
}
@Override
- public int getFlag(final TypedArray a, final int index) {
+ public int getFlags(final TypedArray a, final int index) {
return a.getInt(index, 0);
}
}
@@ -123,14 +123,12 @@ public final class KeyStylesSet {
}
@Override
- public int getFlag(final TypedArray a, final int index) {
- int flags = a.getInt(index, 0);
- final Object value = mStyleAttributes.get(index);
- if (value != null) {
- flags |= (Integer)value;
- }
- final KeyStyle parentStyle = mStyles.get(mParentStyleName);
- return flags | parentStyle.getFlag(a, index);
+ public int getFlags(final TypedArray a, final int index) {
+ final int parentFlags = mStyles.get(mParentStyleName).getFlags(a, index);
+ final Integer value = (Integer)mStyleAttributes.get(index);
+ final int styleFlags = (value != null) ? value : 0;
+ final int flags = a.getInt(index, 0);
+ return flags | styleFlags | parentFlags;
}
public void readKeyAttributes(final TypedArray keyAttr) {
@@ -142,13 +140,13 @@ public final class KeyStylesSet {
readString(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
readStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys);
readStringArray(keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys);
- readFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags);
+ 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);
readInt(keyAttr, R.styleable.Keyboard_Key_backgroundType);
- readFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
+ readFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
}
private void readString(final TypedArray a, final int index) {
@@ -163,10 +161,11 @@ public final class KeyStylesSet {
}
}
- private void readFlag(final TypedArray a, final int index) {
+ private void readFlags(final TypedArray a, final int index) {
if (a.hasValue(index)) {
final Integer value = (Integer)mStyleAttributes.get(index);
- mStyleAttributes.put(index, a.getInt(index, 0) | (value != null ? value : 0));
+ final int styleFlags = value != null ? value : 0;
+ mStyleAttributes.put(index, a.getInt(index, 0) | styleFlags);
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java
index 6ddd2a6fb..7a2622cbb 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java
@@ -21,7 +21,7 @@ import android.graphics.Typeface;
import android.util.SparseIntArray;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResourceUtils;
+import com.android.inputmethod.latin.utils.ResourceUtils;
public final class KeyVisualAttributes {
public final Typeface mTypeface;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
index be178f516..22f7a83fc 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -29,12 +29,13 @@ import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardId;
-import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
+import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResourceUtils;
-import com.android.inputmethod.latin.StringUtils;
-import com.android.inputmethod.latin.SubtypeLocale;
-import com.android.inputmethod.latin.XmlParseUtils;
+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 org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -113,6 +114,7 @@ import java.util.Locale;
* </pre>
*/
+// TODO: Write unit tests for this class.
public class KeyboardBuilder<KP extends KeyboardParams> {
private static final String BUILDER_TAG = "Keyboard.Builder";
private static final boolean DEBUG = false;
@@ -120,6 +122,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
// Keyboard XML Tags
private static final String TAG_KEYBOARD = "Keyboard";
private static final String TAG_ROW = "Row";
+ private static final String TAG_GRID_ROWS = "GridRows";
private static final String TAG_KEY = "Key";
private static final String TAG_SPACER = "Spacer";
private static final String TAG_INCLUDE = "include";
@@ -218,20 +221,18 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
parseKeyboardAttributes(parser);
startKeyboard();
parseKeyboardContent(parser, false);
- break;
- } else {
- throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD);
+ return;
}
+ throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD);
}
}
}
private void parseKeyboardAttributes(final XmlPullParser parser) {
+ final AttributeSet attr = Xml.asAttributeSet(parser);
final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
- Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
- R.style.Keyboard);
- final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Key);
+ attr, R.styleable.Keyboard, R.attr.keyboardStyle, R.style.Keyboard);
+ final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
try {
final KeyboardParams params = mParams;
final int height = params.mId.mHeight;
@@ -279,13 +280,13 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
params.mTextsSet.setLanguage(language);
final RunInLocale<Void> job = new RunInLocale<Void>() {
@Override
- protected Void job(Resources res) {
+ protected Void job(final Resources res) {
params.mTextsSet.loadStringResources(mContext);
return null;
}
};
// Null means the current system locale.
- final Locale locale = SubtypeLocale.isNoLanguage(params.mId.mSubtype)
+ final Locale locale = SubtypeLocaleUtils.isNoLanguage(params.mId.mSubtype)
? null : params.mId.mLocale;
job.runInLocale(mResources, locale);
@@ -314,6 +315,9 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
startRow(row);
}
parseRowContent(parser, row, skip);
+ } else if (TAG_GRID_ROWS.equals(tag)) {
+ if (DEBUG) startTag("<%s>%s", TAG_GRID_ROWS, skip ? " skipped" : "");
+ parseGridRows(parser, skip);
} else if (TAG_INCLUDE.equals(tag)) {
parseIncludeKeyboardContent(parser, skip);
} else if (TAG_SWITCH.equals(tag)) {
@@ -328,31 +332,30 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
if (DEBUG) endTag("</%s>", tag);
if (TAG_KEYBOARD.equals(tag)) {
endKeyboard();
- break;
- } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
- || TAG_MERGE.equals(tag)) {
- break;
- } else {
- throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
+ return;
+ }
+ if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
+ return;
}
+ throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
}
}
}
private KeyboardRow parseRowAttributes(final XmlPullParser parser)
throws XmlPullParserException {
- final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard);
+ final AttributeSet attr = Xml.asAttributeSet(parser);
+ final TypedArray keyboardAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard);
try {
- if (a.hasValue(R.styleable.Keyboard_horizontalGap)) {
+ if (keyboardAttr.hasValue(R.styleable.Keyboard_horizontalGap)) {
throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "horizontalGap");
}
- if (a.hasValue(R.styleable.Keyboard_verticalGap)) {
+ if (keyboardAttr.hasValue(R.styleable.Keyboard_verticalGap)) {
throw new XmlParseUtils.IllegalAttribute(parser, TAG_ROW, "verticalGap");
}
return new KeyboardRow(mResources, mParams, parser, mCurrentY);
} finally {
- a.recycle();
+ keyboardAttr.recycle();
}
}
@@ -382,34 +385,97 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
if (!skip) {
endRow(row);
}
- break;
- } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag)
- || TAG_MERGE.equals(tag)) {
- break;
- } else {
- throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
+ return;
+ }
+ if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) || TAG_MERGE.equals(tag)) {
+ return;
}
+ throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_ROW);
}
}
}
- private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+ private void parseGridRows(final XmlPullParser parser, final boolean skip)
throws XmlPullParserException, IOException {
if (skip) {
- XmlParseUtils.checkEndTag(TAG_KEY, parser);
+ XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser);
if (DEBUG) {
- startEndTag("<%s /> skipped", TAG_KEY);
+ startEndTag("<%s /> skipped", TAG_GRID_ROWS);
}
- } else {
- final Key key = new Key(mResources, mParams, row, parser);
- if (DEBUG) {
- startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY,
- (key.isEnabled() ? "" : " disabled"), key,
- Arrays.toString(key.mMoreKeys));
+ return;
+ }
+ final KeyboardRow gridRows = new KeyboardRow(mResources, mParams, parser, mCurrentY);
+ final TypedArray gridRowAttr = mResources.obtainAttributes(
+ Xml.asAttributeSet(parser), R.styleable.Keyboard_GridRows);
+ final int codesArrayId = gridRowAttr.getResourceId(
+ R.styleable.Keyboard_GridRows_codesArray, 0);
+ final int textsArrayId = gridRowAttr.getResourceId(
+ R.styleable.Keyboard_GridRows_textsArray, 0);
+ gridRowAttr.recycle();
+ if (codesArrayId == 0 && textsArrayId == 0) {
+ throw new XmlParseUtils.ParseException(
+ "Missing codesArray or textsArray attributes", parser);
+ }
+ if (codesArrayId != 0 && textsArrayId != 0) {
+ throw new XmlParseUtils.ParseException(
+ "Both codesArray and textsArray attributes specifed", parser);
+ }
+ final String[] array = mResources.getStringArray(
+ codesArrayId != 0 ? codesArrayId : textsArrayId);
+ final int counts = array.length;
+ final float keyWidth = gridRows.getKeyWidth(null, 0.0f);
+ final int numColumns = (int)(mParams.mOccupiedWidth / keyWidth);
+ for (int index = 0; index < counts; index += numColumns) {
+ final KeyboardRow row = new KeyboardRow(mResources, mParams, parser, mCurrentY);
+ startRow(row);
+ for (int c = 0; c < numColumns; c++) {
+ final int i = index + c;
+ if (i >= counts) {
+ break;
+ }
+ final String label;
+ final int code;
+ final String outputText;
+ if (codesArrayId != 0) {
+ final String codeArraySpec = array[i];
+ label = CodesArrayParser.parseLabel(codeArraySpec);
+ code = CodesArrayParser.parseCode(codeArraySpec);
+ outputText = CodesArrayParser.parseOutputText(codeArraySpec);
+ } else {
+ final String textArraySpec = array[i];
+ // TODO: Utilize KeySpecParser or write more generic TextsArrayParser.
+ label = textArraySpec;
+ code = Constants.CODE_OUTPUT_TEXT;
+ outputText = textArraySpec + (char)Constants.CODE_SPACE;
+ }
+ 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());
+ endKey(key);
+ row.advanceXPos(keyWidth);
}
+ endRow(row);
+ }
+
+ XmlParseUtils.checkEndTag(TAG_GRID_ROWS, parser);
+ }
+
+ private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+ throws XmlPullParserException, IOException {
+ if (skip) {
XmlParseUtils.checkEndTag(TAG_KEY, parser);
- endKey(key);
+ if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
+ return;
}
+ final Key key = new Key(mResources, mParams, row, parser);
+ if (DEBUG) {
+ startEndTag("<%s%s %s moreKeys=%s />", TAG_KEY, (key.isEnabled() ? "" : " disabled"),
+ key, Arrays.toString(key.getMoreKeys()));
+ }
+ XmlParseUtils.checkEndTag(TAG_KEY, parser);
+ endKey(key);
}
private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
@@ -417,12 +483,12 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
if (skip) {
XmlParseUtils.checkEndTag(TAG_SPACER, parser);
if (DEBUG) startEndTag("<%s /> skipped", TAG_SPACER);
- } else {
- final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser);
- if (DEBUG) startEndTag("<%s />", TAG_SPACER);
- XmlParseUtils.checkEndTag(TAG_SPACER, parser);
- endKey(spacer);
+ return;
}
+ final Key.Spacer spacer = new Key.Spacer(mResources, mParams, row, parser);
+ if (DEBUG) startEndTag("<%s />", TAG_SPACER);
+ XmlParseUtils.checkEndTag(TAG_SPACER, parser);
+ endKey(spacer);
}
private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
@@ -440,66 +506,44 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
if (skip) {
XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
if (DEBUG) startEndTag("</%s> skipped", TAG_INCLUDE);
- } else {
- final AttributeSet attr = Xml.asAttributeSet(parser);
- final TypedArray keyboardAttr = mResources.obtainAttributes(attr,
- R.styleable.Keyboard_Include);
- final TypedArray keyAttr = mResources.obtainAttributes(attr,
- R.styleable.Keyboard_Key);
- int keyboardLayout = 0;
- float savedDefaultKeyWidth = 0;
- int savedDefaultKeyLabelFlags = 0;
- int savedDefaultBackgroundType = Key.BACKGROUND_TYPE_NORMAL;
- try {
- XmlParseUtils.checkAttributeExists(keyboardAttr,
- R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
- TAG_INCLUDE, parser);
- keyboardLayout = keyboardAttr.getResourceId(
- R.styleable.Keyboard_Include_keyboardLayout, 0);
- if (row != null) {
- if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
- // Override current x coordinate.
- row.setXPos(row.getKeyX(keyAttr));
- }
- // TODO: Remove this if-clause and do the same as backgroundType below.
- savedDefaultKeyWidth = row.getDefaultKeyWidth();
- if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyWidth)) {
- // Override default key width.
- row.setDefaultKeyWidth(row.getKeyWidth(keyAttr));
- }
- savedDefaultKeyLabelFlags = row.getDefaultKeyLabelFlags();
- // Bitwise-or default keyLabelFlag if exists.
- row.setDefaultKeyLabelFlags(keyAttr.getInt(
- R.styleable.Keyboard_Key_keyLabelFlags, 0)
- | savedDefaultKeyLabelFlags);
- savedDefaultBackgroundType = row.getDefaultBackgroundType();
- // Override default backgroundType if exists.
- row.setDefaultBackgroundType(keyAttr.getInt(
- R.styleable.Keyboard_Key_backgroundType,
- savedDefaultBackgroundType));
- }
- } finally {
- keyboardAttr.recycle();
- keyAttr.recycle();
+ return;
+ }
+ final AttributeSet attr = Xml.asAttributeSet(parser);
+ final TypedArray keyboardAttr = mResources.obtainAttributes(
+ attr, R.styleable.Keyboard_Include);
+ final TypedArray keyAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
+ int keyboardLayout = 0;
+ try {
+ XmlParseUtils.checkAttributeExists(
+ keyboardAttr, R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout",
+ TAG_INCLUDE, parser);
+ keyboardLayout = keyboardAttr.getResourceId(
+ R.styleable.Keyboard_Include_keyboardLayout, 0);
+ if (row != null) {
+ // Override current x coordinate.
+ row.setXPos(row.getKeyX(keyAttr));
+ // Push current Row attributes and update with new attributes.
+ row.pushRowAttributes(keyAttr);
}
+ } finally {
+ keyboardAttr.recycle();
+ keyAttr.recycle();
+ }
- XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
- if (DEBUG) {
- startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
- mResources.getResourceEntryName(keyboardLayout));
- }
- final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
- try {
- parseMerge(parserForInclude, row, skip);
- } finally {
- if (row != null) {
- // Restore default keyWidth, keyLabelFlags, and backgroundType.
- row.setDefaultKeyWidth(savedDefaultKeyWidth);
- row.setDefaultKeyLabelFlags(savedDefaultKeyLabelFlags);
- row.setDefaultBackgroundType(savedDefaultBackgroundType);
- }
- parserForInclude.close();
+ XmlParseUtils.checkEndTag(TAG_INCLUDE, parser);
+ if (DEBUG) {
+ startEndTag("<%s keyboardLayout=%s />",TAG_INCLUDE,
+ mResources.getResourceEntryName(keyboardLayout));
+ }
+ final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout);
+ try {
+ parseMerge(parserForInclude, row, skip);
+ } finally {
+ if (row != null) {
+ // Restore Row attributes.
+ row.popRowAttributes();
}
+ parserForInclude.close();
}
}
@@ -516,11 +560,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
} else {
parseRowContent(parser, row, skip);
}
- break;
- } else {
- throw new XmlParseUtils.ParseException(
- "Included keyboard layout must have <merge> root element", parser);
+ return;
}
+ throw new XmlParseUtils.ParseException(
+ "Included keyboard layout must have <merge> root element", parser);
}
}
}
@@ -554,10 +597,9 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
final String tag = parser.getName();
if (TAG_SWITCH.equals(tag)) {
if (DEBUG) endTag("</%s>", TAG_SWITCH);
- break;
- } else {
- throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH);
+ return;
}
+ throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_SWITCH);
}
}
}
@@ -580,86 +622,92 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
if (id == null) {
return true;
}
- final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Case);
+ final AttributeSet attr = Xml.asAttributeSet(parser);
+ final TypedArray caseAttr = mResources.obtainAttributes(attr, R.styleable.Keyboard_Case);
try {
- final boolean keyboardLayoutSetElementMatched = matchTypedValue(a,
+ final boolean keyboardLayoutSetMatched = matchString(caseAttr,
+ R.styleable.Keyboard_Case_keyboardLayoutSet,
+ SubtypeLocaleUtils.getKeyboardLayoutSetName(id.mSubtype));
+ final boolean keyboardLayoutSetElementMatched = matchTypedValue(caseAttr,
R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
KeyboardId.elementIdToName(id.mElementId));
- final boolean modeMatched = matchTypedValue(a,
+ final boolean modeMatched = matchTypedValue(caseAttr,
R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
- final boolean navigateNextMatched = matchBoolean(a,
+ final boolean navigateNextMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
- final boolean navigatePreviousMatched = matchBoolean(a,
+ final boolean navigatePreviousMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
- final boolean passwordInputMatched = matchBoolean(a,
+ final boolean passwordInputMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
- final boolean clobberSettingsKeyMatched = matchBoolean(a,
+ final boolean clobberSettingsKeyMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
- final boolean shortcutKeyEnabledMatched = matchBoolean(a,
+ final boolean shortcutKeyEnabledMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
- final boolean shortcutKeyOnSymbolsMatched = matchBoolean(a,
+ final boolean shortcutKeyOnSymbolsMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_shortcutKeyOnSymbols, id.mShortcutKeyOnSymbols);
- final boolean hasShortcutKeyMatched = matchBoolean(a,
+ final boolean hasShortcutKeyMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
- final boolean languageSwitchKeyEnabledMatched = matchBoolean(a,
+ final boolean languageSwitchKeyEnabledMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
id.mLanguageSwitchKeyEnabled);
- final boolean isMultiLineMatched = matchBoolean(a,
+ final boolean isMultiLineMatched = matchBoolean(caseAttr,
R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
- final boolean imeActionMatched = matchInteger(a,
+ final boolean imeActionMatched = matchInteger(caseAttr,
R.styleable.Keyboard_Case_imeAction, id.imeAction());
- final boolean localeCodeMatched = matchString(a,
+ final boolean localeCodeMatched = matchString(caseAttr,
R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
- final boolean languageCodeMatched = matchString(a,
+ final boolean languageCodeMatched = matchString(caseAttr,
R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
- final boolean countryCodeMatched = matchString(a,
+ final boolean countryCodeMatched = matchString(caseAttr,
R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
- final boolean selected = keyboardLayoutSetElementMatched && modeMatched
- && navigateNextMatched && navigatePreviousMatched && passwordInputMatched
- && clobberSettingsKeyMatched && shortcutKeyEnabledMatched
- && shortcutKeyOnSymbolsMatched && hasShortcutKeyMatched
- && languageSwitchKeyEnabledMatched && isMultiLineMatched && imeActionMatched
- && localeCodeMatched && languageCodeMatched && countryCodeMatched;
+ final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched
+ && modeMatched && navigateNextMatched && navigatePreviousMatched
+ && passwordInputMatched && clobberSettingsKeyMatched
+ && shortcutKeyEnabledMatched && shortcutKeyOnSymbolsMatched
+ && 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", TAG_CASE,
- textAttr(a.getString(
+ startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
+ textAttr(caseAttr.getString(
+ R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"),
+ textAttr(caseAttr.getString(
R.styleable.Keyboard_Case_keyboardLayoutSetElement),
"keyboardLayoutSetElement"),
- textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
- textAttr(a.getString(R.styleable.Keyboard_Case_imeAction),
+ textAttr(caseAttr.getString(R.styleable.Keyboard_Case_mode), "mode"),
+ textAttr(caseAttr.getString(R.styleable.Keyboard_Case_imeAction),
"imeAction"),
- booleanAttr(a, R.styleable.Keyboard_Case_navigateNext,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigateNext,
"navigateNext"),
- booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_navigatePrevious,
"navigatePrevious"),
- booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_clobberSettingsKey,
"clobberSettingsKey"),
- booleanAttr(a, R.styleable.Keyboard_Case_passwordInput,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_passwordInput,
"passwordInput"),
- booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_shortcutKeyEnabled,
"shortcutKeyEnabled"),
- booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyOnSymbols,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_shortcutKeyOnSymbols,
"shortcutKeyOnSymbols"),
- booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_hasShortcutKey,
"hasShortcutKey"),
- booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
"languageSwitchKeyEnabled"),
- booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine,
+ booleanAttr(caseAttr, R.styleable.Keyboard_Case_isMultiLine,
"isMultiLine"),
- textAttr(a.getString(R.styleable.Keyboard_Case_localeCode),
+ textAttr(caseAttr.getString(R.styleable.Keyboard_Case_localeCode),
"localeCode"),
- textAttr(a.getString(R.styleable.Keyboard_Case_languageCode),
+ textAttr(caseAttr.getString(R.styleable.Keyboard_Case_languageCode),
"languageCode"),
- textAttr(a.getString(R.styleable.Keyboard_Case_countryCode),
+ textAttr(caseAttr.getString(R.styleable.Keyboard_Case_countryCode),
"countryCode"),
selected ? "" : " skipped");
}
return selected;
} finally {
- a.recycle();
+ caseAttr.recycle();
}
}
@@ -692,7 +740,8 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
}
if (ResourceUtils.isIntegerValue(v)) {
return intValue == a.getInt(index, 0);
- } else if (ResourceUtils.isStringValue(v)) {
+ }
+ if (ResourceUtils.isStringValue(v)) {
return StringUtils.containsInArray(strValue, a.getString(index).split("\\|"));
}
return false;
@@ -711,10 +760,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
private void parseKeyStyle(final XmlPullParser parser, final boolean skip)
throws XmlPullParserException, IOException {
- TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_KeyStyle);
- TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Key);
+ final AttributeSet attr = Xml.asAttributeSet(parser);
+ final TypedArray keyStyleAttr = mResources.obtainAttributes(
+ attr, R.styleable.Keyboard_KeyStyle);
+ final TypedArray keyAttrs = mResources.obtainAttributes(attr, R.styleable.Keyboard_Key);
try {
if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) {
throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
@@ -756,7 +805,7 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
mRightEdgeKey = null;
}
addEdgeSpace(mParams.mRightPadding, row);
- mCurrentY += row.mRowHeight;
+ mCurrentY += row.getRowHeight();
mCurrentRow = null;
mTopEdge = false;
}
@@ -774,7 +823,10 @@ public class KeyboardBuilder<KP extends KeyboardParams> {
}
private void endKeyboard() {
- // nothing to do here.
+ // {@link #parseGridRows(XmlPullParser,boolean)} may populate keyboard rows higher than
+ // previously expected.
+ final int actualHeight = mCurrentY - mParams.mVerticalGap + mParams.mBottomPadding;
+ mParams.mOccupiedHeight = Math.max(mParams.mOccupiedHeight, actualHeight);
}
private void addEdgeSpace(final float width, final KeyboardRow row) {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
index a9e04bccf..dc815e57d 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
@@ -16,8 +16,8 @@
package com.android.inputmethod.keyboard.internal;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.HashMap;
@@ -53,7 +53,7 @@ public final class KeyboardCodesSet {
"key_action_previous",
"key_shift_enter",
"key_language_switch",
- "key_research",
+ "key_emoji",
"key_unspecified",
"key_left_parenthesis",
"key_right_parenthesis",
@@ -90,7 +90,7 @@ public final class KeyboardCodesSet {
Constants.CODE_ACTION_PREVIOUS,
Constants.CODE_SHIFT_ENTER,
Constants.CODE_LANGUAGE_SWITCH,
- Constants.CODE_RESEARCH,
+ Constants.CODE_EMOJI,
Constants.CODE_UNSPECIFIED,
CODE_LEFT_PARENTHESIS,
CODE_RIGHT_PARENTHESIS,
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
index 4ac2549c7..336db186e 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
@@ -22,8 +22,8 @@ import android.graphics.drawable.Drawable;
import android.util.Log;
import android.util.SparseIntArray;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.HashMap;
@@ -56,6 +56,7 @@ public final class KeyboardIconsSet {
"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,
};
private static int NUM_ICONS = NAMES_AND_ATTR_IDS.length / 2;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
index 15eb690e1..d32bb7581 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
@@ -20,8 +20,8 @@ import android.util.SparseIntArray;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.KeyboardId;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.ArrayList;
import java.util.TreeSet;
@@ -84,12 +84,17 @@ public class KeyboardParams {
public void onAddKey(final Key newKey) {
final Key key = (mKeysCache != null) ? mKeysCache.get(newKey) : newKey;
- final boolean zeroWidthSpacer = key.isSpacer() && key.mWidth == 0;
- if (!zeroWidthSpacer) {
- mKeys.add(key);
- updateHistogram(key);
+ final boolean isSpacer = key.isSpacer();
+ if (isSpacer && key.getWidth() == 0) {
+ // Ignore zero width {@link Spacer}.
+ return;
}
- if (key.mCode == Constants.CODE_SHIFT) {
+ mKeys.add(key);
+ if (isSpacer) {
+ return;
+ }
+ updateHistogram(key);
+ if (key.getCode() == Constants.CODE_SHIFT) {
mShiftKeys.add(key);
}
if (key.altCodeWhileTyping()) {
@@ -120,14 +125,14 @@ public class KeyboardParams {
}
private void updateHistogram(final Key key) {
- final int height = key.mHeight + mVerticalGap;
+ final int height = key.getHeight() + mVerticalGap;
final int heightCount = updateHistogramCounter(mHeightHistogram, height);
if (heightCount > mMaxHeightCount) {
mMaxHeightCount = heightCount;
mMostCommonKeyHeight = height;
}
- final int width = key.mWidth + mHorizontalGap;
+ final int width = key.getWidth() + mHorizontalGap;
final int widthCount = updateHistogramCounter(mWidthHistogram, width);
if (widthCount > mMaxWidthCount) {
mMaxWidthCount = widthCount;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
index 855f65507..0f9497c27 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
@@ -23,10 +23,13 @@ import android.util.Xml;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResourceUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.ResourceUtils;
import org.xmlpull.v1.XmlPullParser;
+import java.util.ArrayDeque;
+
/**
* Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
* Some of the key size defaults can be overridden per row from what the {@link Keyboard}
@@ -38,64 +41,100 @@ public final class KeyboardRow {
private static final int KEYWIDTH_FILL_RIGHT = -1;
private final KeyboardParams mParams;
- /** Default width of a key in this row. */
- private float mDefaultKeyWidth;
- /** Default height of a key in this row. */
- public final int mRowHeight;
- /** Default keyLabelFlags in this row. */
- private int mDefaultKeyLabelFlags;
- /** Default backgroundType for this row */
- private int mDefaultBackgroundType;
+ /** The height of this row. */
+ private final int mRowHeight;
+
+ private final ArrayDeque<RowAttributes> mRowAttributesStack = CollectionUtils.newArrayDeque();
+
+ private static class RowAttributes {
+ /** Default width of a key in this row. */
+ public final float mDefaultKeyWidth;
+ /** Default keyLabelFlags in this row. */
+ public final int mDefaultKeyLabelFlags;
+ /** Default backgroundType for this row */
+ public final int mDefaultBackgroundType;
+
+ /**
+ * Parse and create key attributes. This constructor is used to parse Row tag.
+ *
+ * @param keyAttr an attributes array of Row tag.
+ * @param defaultKeyWidth a default key width.
+ * @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute.
+ */
+ public RowAttributes(final TypedArray keyAttr, final float defaultKeyWidth,
+ final int keyboardWidth) {
+ mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+ keyboardWidth, keyboardWidth, defaultKeyWidth);
+ mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0);
+ mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
+ Key.BACKGROUND_TYPE_NORMAL);
+ }
+
+ /**
+ * Parse and update key attributes using default attributes. This constructor is used
+ * to parse include tag.
+ *
+ * @param keyAttr an attributes array of include tag.
+ * @param defaultRowAttr default Row attributes.
+ * @param keyboardWidth the keyboard width that is required to calculate keyWidth attribute.
+ */
+ public RowAttributes(final TypedArray keyAttr, final RowAttributes defaultRowAttr,
+ final int keyboardWidth) {
+ mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+ keyboardWidth, keyboardWidth, defaultRowAttr.mDefaultKeyWidth);
+ mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0)
+ | defaultRowAttr.mDefaultKeyLabelFlags;
+ mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
+ defaultRowAttr.mDefaultBackgroundType);
+ }
+ }
private final int mCurrentY;
// Will be updated by {@link Key}'s constructor.
private float mCurrentX;
- public KeyboardRow(final Resources res, final KeyboardParams params, final XmlPullParser parser,
- final int y) {
+ public KeyboardRow(final Resources res, final KeyboardParams params,
+ final XmlPullParser parser, final int y) {
mParams = params;
final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard);
mRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_rowHeight,
- params.mBaseHeight, params.mDefaultRowHeight);
+ R.styleable.Keyboard_rowHeight, params.mBaseHeight, params.mDefaultRowHeight);
keyboardAttr.recycle();
final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard_Key);
- mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
- params.mBaseWidth, params.mBaseWidth, params.mDefaultKeyWidth);
- mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
- Key.BACKGROUND_TYPE_NORMAL);
+ mRowAttributesStack.push(new RowAttributes(
+ keyAttr, params.mDefaultKeyWidth, params.mBaseWidth));
keyAttr.recycle();
- // TODO: Initialize this with <Row> attribute as backgroundType is done.
- mDefaultKeyLabelFlags = 0;
mCurrentY = y;
mCurrentX = 0.0f;
}
- public float getDefaultKeyWidth() {
- return mDefaultKeyWidth;
+ public int getRowHeight() {
+ return mRowHeight;
}
- public void setDefaultKeyWidth(final float defaultKeyWidth) {
- mDefaultKeyWidth = defaultKeyWidth;
+ public void pushRowAttributes(final TypedArray keyAttr) {
+ final RowAttributes newAttributes = new RowAttributes(
+ keyAttr, mRowAttributesStack.peek(), mParams.mBaseWidth);
+ mRowAttributesStack.push(newAttributes);
}
- public int getDefaultKeyLabelFlags() {
- return mDefaultKeyLabelFlags;
+ public void popRowAttributes() {
+ mRowAttributesStack.pop();
}
- public void setDefaultKeyLabelFlags(final int keyLabelFlags) {
- mDefaultKeyLabelFlags = keyLabelFlags;
+ public float getDefaultKeyWidth() {
+ return mRowAttributesStack.peek().mDefaultKeyWidth;
}
- public int getDefaultBackgroundType() {
- return mDefaultBackgroundType;
+ public int getDefaultKeyLabelFlags() {
+ return mRowAttributesStack.peek().mDefaultKeyLabelFlags;
}
- public void setDefaultBackgroundType(final int backgroundType) {
- mDefaultBackgroundType = backgroundType;
+ public int getDefaultBackgroundType() {
+ return mRowAttributesStack.peek().mDefaultBackgroundType;
}
public void setXPos(final float keyXPos) {
@@ -111,29 +150,27 @@ public final class KeyboardRow {
}
public float getKeyX(final TypedArray keyAttr) {
- if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
- final float keyXPos = keyAttr.getFraction(R.styleable.Keyboard_Key_keyXPos,
- mParams.mBaseWidth, mParams.mBaseWidth, 0);
- if (keyXPos < 0) {
- // If keyXPos is negative, the actual x-coordinate will be
- // keyboardWidth + keyXPos.
- // keyXPos shouldn't be less than mCurrentX because drawable area for this
- // key starts at mCurrentX. Or, this key will overlaps the adjacent key on
- // its left hand side.
- final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
- return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
- } else {
- return keyXPos + mParams.mLeftPadding;
- }
+ if (keyAttr == null || !keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
+ return mCurrentX;
}
- return mCurrentX;
- }
-
- public float getKeyWidth(final TypedArray keyAttr) {
- return getKeyWidth(keyAttr, mCurrentX);
+ final float keyXPos = keyAttr.getFraction(R.styleable.Keyboard_Key_keyXPos,
+ mParams.mBaseWidth, mParams.mBaseWidth, 0);
+ if (keyXPos >= 0) {
+ return keyXPos + mParams.mLeftPadding;
+ }
+ // If keyXPos is negative, the actual x-coordinate will be
+ // keyboardWidth + keyXPos.
+ // keyXPos shouldn't be less than mCurrentX because drawable area for this
+ // key starts at mCurrentX. Or, this key will overlaps the adjacent key on
+ // its left hand side.
+ final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
+ return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
}
public float getKeyWidth(final TypedArray keyAttr, final float keyXPos) {
+ if (keyAttr == null) {
+ return getDefaultKeyWidth();
+ }
final int widthType = ResourceUtils.getEnumValue(keyAttr,
R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
switch (widthType) {
@@ -144,7 +181,7 @@ public final class KeyboardRow {
return keyboardRightEdge - keyXPos;
default: // KEYWIDTH_NOT_ENUM
return keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
- mParams.mBaseWidth, mParams.mBaseWidth, mDefaultKeyWidth);
+ mParams.mBaseWidth, mParams.mBaseWidth, getDefaultKeyWidth());
}
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index e1cee427e..9f9fdaa6f 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -20,7 +20,7 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.RecapitalizeStatus;
+import com.android.inputmethod.latin.utils.RecapitalizeStatus;
/**
* Keyboard state machine.
@@ -29,8 +29,8 @@ import com.android.inputmethod.latin.RecapitalizeStatus;
*
* The input events are {@link #onLoadKeyboard()}, {@link #onSaveKeyboardState()},
* {@link #onPressKey(int,boolean,int)}, {@link #onReleaseKey(int,boolean)},
- * {@link #onCodeInput(int,int)}, {@link #onFinishSlidingInput()}, {@link #onCancelInput()},
- * {@link #onUpdateShiftState(int,int)}.
+ * {@link #onCodeInput(int,int)}, {@link #onFinishSlidingInput()},
+ * {@link #onUpdateShiftState(int,int)}, {@link #onResetKeyboardStateToAlphabet()}.
*
* The actions are {@link SwitchActions}'s methods.
*/
@@ -45,6 +45,7 @@ public final class KeyboardState {
public void setAlphabetAutomaticShiftedKeyboard();
public void setAlphabetShiftLockedKeyboard();
public void setAlphabetShiftLockShiftedKeyboard();
+ public void setEmojiKeyboard();
public void setSymbolsKeyboard();
public void setSymbolsShiftedKeyboard();
@@ -74,16 +75,16 @@ public final class KeyboardState {
private static final int SWITCH_STATE_MOMENTARY_ALPHA_SHIFT = 5;
private int mSwitchState = SWITCH_STATE_ALPHA;
+ // TODO: Consolidate these two mode booleans into one integer to distinguish between alphabet,
+ // symbols, and emoji mode.
private boolean mIsAlphabetMode;
+ private boolean mIsEmojiMode;
private AlphabetShiftState mAlphabetShiftState = new AlphabetShiftState();
private boolean mIsSymbolShifted;
private boolean mPrevMainKeyboardWasShiftLocked;
private boolean mPrevSymbolsKeyboardWasShifted;
private int mRecapitalizeMode;
- // For handling long press.
- private boolean mLongPressShiftLockFired;
-
// For handling double tap.
private boolean mIsInAlphabetUnshiftedFromShifted;
private boolean mIsInDoubleTapShiftKey;
@@ -94,6 +95,7 @@ public final class KeyboardState {
public boolean mIsValid;
public boolean mIsAlphabetMode;
public boolean mIsAlphabetShiftLocked;
+ public boolean mIsEmojiMode;
public int mShiftMode;
@Override
@@ -102,6 +104,8 @@ public final class KeyboardState {
if (mIsAlphabetMode) {
if (mIsAlphabetShiftLocked) return "ALPHABET_SHIFT_LOCKED";
return "ALPHABET_" + shiftModeToString(mShiftMode);
+ } else if (mIsEmojiMode) {
+ return "EMOJI";
} else {
return "SYMBOLS_" + shiftModeToString(mShiftMode);
}
@@ -134,6 +138,7 @@ public final class KeyboardState {
public void onSaveKeyboardState() {
final SavedKeyboardState state = mSavedKeyboardState;
state.mIsAlphabetMode = mIsAlphabetMode;
+ state.mIsEmojiMode = mIsEmojiMode;
if (mIsAlphabetMode) {
state.mIsAlphabetShiftLocked = mAlphabetShiftState.isShiftLocked();
state.mShiftMode = mAlphabetShiftState.isAutomaticShifted() ? AUTOMATIC_SHIFT
@@ -155,6 +160,8 @@ public final class KeyboardState {
}
if (!state.mIsValid || state.mIsAlphabetMode) {
setAlphabetKeyboard();
+ } else if (state.mIsEmojiMode) {
+ setEmojiKeyboard();
} else {
if (state.mShiftMode == MANUAL_SHIFT) {
setSymbolsShiftedKeyboard();
@@ -283,6 +290,7 @@ public final class KeyboardState {
mSwitchActions.setAlphabetKeyboard();
mIsAlphabetMode = true;
+ mIsEmojiMode = false;
mIsSymbolShifted = false;
mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
mSwitchState = SWITCH_STATE_ALPHA;
@@ -313,6 +321,15 @@ public final class KeyboardState {
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
}
+ private void setEmojiKeyboard() {
+ if (DEBUG_ACTION) {
+ Log.d(TAG, "setEmojiKeyboard");
+ }
+ mIsAlphabetMode = false;
+ mIsEmojiMode = true;
+ mSwitchActions.setEmojiKeyboard();
+ }
+
public void onPressKey(final int code, final boolean isSinglePointer, final int autoCaps) {
if (DEBUG_EVENT) {
Log.d(TAG, "onPressKey: code=" + Constants.printableCode(code)
@@ -325,10 +342,11 @@ public final class KeyboardState {
}
if (code == Constants.CODE_SHIFT) {
onPressShift();
+ } else if (code == Constants.CODE_CAPSLOCK) {
+ // Nothing to do here. See {@link #onReleaseKey(int,boolean)}.
} else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
onPressSymbol();
} else {
- mLongPressShiftLockFired = false;
mShiftKeyState.onOtherKeyPressed();
mSymbolKeyState.onOtherKeyPressed();
// It is required to reset the auto caps state when all of the following conditions
@@ -356,6 +374,8 @@ public final class KeyboardState {
}
if (code == Constants.CODE_SHIFT) {
onReleaseShift(withSliding);
+ } else if (code == Constants.CODE_CAPSLOCK) {
+ setShiftLocked(!mAlphabetShiftState.isShiftLocked());
} else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
onReleaseSymbol(withSliding);
}
@@ -437,7 +457,6 @@ public final class KeyboardState {
}
private void onPressShift() {
- mLongPressShiftLockFired = false;
// If we are recapitalizing, we don't do any of the normal processing, including
// importantly the double tap timer.
if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) {
@@ -461,7 +480,8 @@ public final class KeyboardState {
} else {
if (mAlphabetShiftState.isShiftLocked()) {
// Shift key is pressed while shift locked state, we will treat this state as
- // shift lock shifted state and mark as if shift key pressed while normal state.
+ // shift lock shifted state and mark as if shift key pressed while normal
+ // state.
setShifted(SHIFT_LOCK_SHIFTED);
mShiftKeyState.onPress();
} else if (mAlphabetShiftState.isAutomaticShifted()) {
@@ -499,8 +519,6 @@ public final class KeyboardState {
// Double tap shift key has been handled in {@link #onPressShift}, so that just
// ignore this release shift key here.
mIsInDoubleTapShiftKey = false;
- } else if (mLongPressShiftLockFired) {
- setShiftLocked(!mAlphabetShiftState.isShiftLocked());
} else if (mShiftKeyState.isChording()) {
if (mAlphabetShiftState.isShiftLockShifted()) {
// After chording input while shift locked state.
@@ -567,7 +585,7 @@ public final class KeyboardState {
}
}
- private static boolean isSpaceCharacter(final int c) {
+ private static boolean isSpaceOrEnter(final int c) {
return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER;
}
@@ -590,12 +608,18 @@ public final class KeyboardState {
break;
case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
if (code == Constants.CODE_SHIFT) {
- // Detected only the shift key has been pressed on symbol layout, and then released.
+ // Detected only the shift key has been pressed on symbol layout, and then
+ // released.
mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
}
break;
case SWITCH_STATE_SYMBOL_BEGIN:
- if (!isSpaceCharacter(code) && (Constants.isLetterCode(code)
+ if (mIsEmojiMode) {
+ // When in the Emoji keyboard, we don't want to switch back to the main layout even
+ // after the user hits an emoji letter followed by an enter or a space.
+ break;
+ }
+ if (!isSpaceOrEnter(code) && (Constants.isLetterCode(code)
|| code == Constants.CODE_OUTPUT_TEXT)) {
mSwitchState = SWITCH_STATE_SYMBOL;
}
@@ -603,22 +627,18 @@ public final class KeyboardState {
case SWITCH_STATE_SYMBOL:
// Switch back to alpha keyboard mode if user types one or more non-space/enter
// characters followed by a space/enter.
- if (isSpaceCharacter(code)) {
+ if (isSpaceOrEnter(code)) {
toggleAlphabetAndSymbols();
mPrevSymbolsKeyboardWasShifted = false;
}
break;
}
- if (code == Constants.CODE_CAPSLOCK) {
- // Changing shift lock state will be handled at {@link #onPressShift()} when the shift
- // key is released.
- mLongPressShiftLockFired = true;
- }
-
// If the code is a letter, update keyboard shift state.
if (Constants.isLetterCode(code)) {
updateAlphabetShiftState(autoCaps, RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE);
+ } else if (code == Constants.CODE_EMOJI) {
+ setEmojiKeyboard();
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index 7ec1c9406..67553fb75 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -20,25 +20,25 @@ import android.content.Context;
import android.content.res.Resources;
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.HashMap;
/**
* !!!!! DO NOT EDIT THIS FILE !!!!!
*
- * This file is generated by tools/maketext. The base template file is
- * tools/maketext/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
+ * 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/maketext/res/values-<locale>/donottranslate-more-keys.xml
+ * 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/maketext
- * $ maketext -java packages/inputmethods/LatinIME/java/src
+ * $ 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/
@@ -133,122 +133,124 @@ public final class KeyboardTextsSet {
/* 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_en",
- /* 32 */ "more_keys_for_cyrillic_ghe",
- /* 33 */ "more_keys_for_east_slavic_row2_1",
- /* 34 */ "more_keys_for_cyrillic_o",
- /* 35 */ "more_keys_for_cyrillic_soft_sign",
- /* 36 */ "keylabel_for_south_slavic_row1_6",
- /* 37 */ "keylabel_for_south_slavic_row2_11",
- /* 38 */ "keylabel_for_south_slavic_row3_1",
- /* 39 */ "keylabel_for_south_slavic_row3_8",
- /* 40 */ "more_keys_for_cyrillic_ie",
- /* 41 */ "more_keys_for_cyrillic_i",
- /* 42 */ "label_to_alpha_key",
- /* 43 */ "single_quotes",
- /* 44 */ "double_quotes",
- /* 45 */ "single_angle_quotes",
- /* 46 */ "double_angle_quotes",
- /* 47 */ "more_keys_for_currency_dollar",
- /* 48 */ "keylabel_for_currency_generic",
- /* 49 */ "more_keys_for_currency_generic",
- /* 50 */ "more_keys_for_punctuation",
- /* 51 */ "more_keys_for_star",
- /* 52 */ "more_keys_for_bullet",
- /* 53 */ "more_keys_for_plus",
- /* 54 */ "more_keys_for_left_parenthesis",
- /* 55 */ "more_keys_for_right_parenthesis",
- /* 56 */ "more_keys_for_less_than",
- /* 57 */ "more_keys_for_greater_than",
- /* 58 */ "more_keys_for_arabic_diacritics",
- /* 59 */ "keyhintlabel_for_arabic_diacritics",
- /* 60 */ "keylabel_for_symbols_1",
- /* 61 */ "keylabel_for_symbols_2",
- /* 62 */ "keylabel_for_symbols_3",
- /* 63 */ "keylabel_for_symbols_4",
- /* 64 */ "keylabel_for_symbols_5",
- /* 65 */ "keylabel_for_symbols_6",
- /* 66 */ "keylabel_for_symbols_7",
- /* 67 */ "keylabel_for_symbols_8",
- /* 68 */ "keylabel_for_symbols_9",
- /* 69 */ "keylabel_for_symbols_0",
- /* 70 */ "label_to_symbol_key",
- /* 71 */ "label_to_symbol_with_microphone_key",
- /* 72 */ "additional_more_keys_for_symbols_1",
- /* 73 */ "additional_more_keys_for_symbols_2",
- /* 74 */ "additional_more_keys_for_symbols_3",
- /* 75 */ "additional_more_keys_for_symbols_4",
- /* 76 */ "additional_more_keys_for_symbols_5",
- /* 77 */ "additional_more_keys_for_symbols_6",
- /* 78 */ "additional_more_keys_for_symbols_7",
- /* 79 */ "additional_more_keys_for_symbols_8",
- /* 80 */ "additional_more_keys_for_symbols_9",
- /* 81 */ "additional_more_keys_for_symbols_0",
- /* 82 */ "more_keys_for_symbols_1",
- /* 83 */ "more_keys_for_symbols_2",
- /* 84 */ "more_keys_for_symbols_3",
- /* 85 */ "more_keys_for_symbols_4",
- /* 86 */ "more_keys_for_symbols_5",
- /* 87 */ "more_keys_for_symbols_6",
- /* 88 */ "more_keys_for_symbols_7",
- /* 89 */ "more_keys_for_symbols_8",
- /* 90 */ "more_keys_for_symbols_9",
- /* 91 */ "more_keys_for_symbols_0",
- /* 92 */ "keylabel_for_comma",
- /* 93 */ "more_keys_for_comma",
- /* 94 */ "keylabel_for_symbols_question",
- /* 95 */ "keylabel_for_symbols_semicolon",
- /* 96 */ "keylabel_for_symbols_percent",
- /* 97 */ "more_keys_for_symbols_exclamation",
- /* 98 */ "more_keys_for_symbols_question",
- /* 99 */ "more_keys_for_symbols_semicolon",
- /* 100 */ "more_keys_for_symbols_percent",
- /* 101 */ "keylabel_for_tablet_comma",
- /* 102 */ "keyhintlabel_for_tablet_comma",
- /* 103 */ "more_keys_for_tablet_comma",
- /* 104 */ "keyhintlabel_for_tablet_period",
- /* 105 */ "more_keys_for_tablet_period",
- /* 106 */ "keylabel_for_apostrophe",
- /* 107 */ "keyhintlabel_for_apostrophe",
- /* 108 */ "more_keys_for_apostrophe",
- /* 109 */ "more_keys_for_q",
- /* 110 */ "more_keys_for_x",
- /* 111 */ "keylabel_for_q",
- /* 112 */ "keylabel_for_w",
- /* 113 */ "keylabel_for_y",
- /* 114 */ "keylabel_for_x",
- /* 115 */ "keylabel_for_spanish_row2_10",
- /* 116 */ "more_keys_for_am_pm",
- /* 117 */ "settings_as_more_key",
- /* 118 */ "shortcut_as_more_key",
- /* 119 */ "action_next_as_more_key",
- /* 120 */ "action_previous_as_more_key",
- /* 121 */ "label_to_more_symbol_key",
- /* 122 */ "label_to_more_symbol_for_tablet_key",
- /* 123 */ "label_tab_key",
- /* 124 */ "label_to_phone_numeric_key",
- /* 125 */ "label_to_phone_symbols_key",
- /* 126 */ "label_time_am",
- /* 127 */ "label_time_pm",
- /* 128 */ "label_to_symbol_key_pcqwerty",
- /* 129 */ "keylabel_for_popular_domain",
- /* 130 */ "more_keys_for_popular_domain",
- /* 131 */ "more_keys_for_smiley",
- /* 132 */ "single_laqm_raqm",
- /* 133 */ "single_laqm_raqm_rtl",
- /* 134 */ "single_raqm_laqm",
- /* 135 */ "double_laqm_raqm",
- /* 136 */ "double_laqm_raqm_rtl",
- /* 137 */ "double_raqm_laqm",
- /* 138 */ "single_lqm_rqm",
- /* 139 */ "single_9qm_lqm",
- /* 140 */ "single_9qm_rqm",
- /* 141 */ "double_lqm_rqm",
- /* 142 */ "double_9qm_lqm",
- /* 143 */ "double_9qm_rqm",
- /* 144 */ "more_keys_for_single_quote",
- /* 145 */ "more_keys_for_double_quote",
- /* 146 */ "more_keys_for_tablet_double_quote",
+ /* 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_tablet_period",
+ /* 108 */ "more_keys_for_tablet_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",
};
private static final String EMPTY = "";
@@ -259,147 +261,145 @@ public final class KeyboardTextsSet {
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,
- /* ~41 */
+ EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
+ /* ~44 */
// Label for "switch to alphabetic" key.
- /* 42 */ "ABC",
- /* 43 */ "!text/single_lqm_rqm",
- /* 44 */ "!text/double_lqm_rqm",
- /* 45 */ "!text/single_laqm_raqm",
- /* 46 */ "!text/double_laqm_raqm",
+ /* 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
- /* 47 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1",
- /* 48 */ "$",
- /* 49 */ "$,\u00A2,\u20AC,\u00A3,\u00A5,\u20B1",
- /* 50 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)",
+ /* 50 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1",
+ /* 51 */ "$",
+ /* 52 */ "$,\u00A2,\u20AC,\u00A3,\u00A5,\u20B1",
+ /* 53 */ "!fixedColumnOrder!3,!,\\,,?,:,;,@",
// U+2020: "†" DAGGER
// U+2021: "‡" DOUBLE DAGGER
// U+2605: "★" BLACK STAR
- /* 51 */ "\u2020,\u2021,\u2605",
+ /* 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
- /* 52 */ "\u266A,\u2665,\u2660,\u2666,\u2663",
+ /* 55 */ "\u266A,\u2665,\u2660,\u2666,\u2663",
// U+00B1: "±" PLUS-MINUS SIGN
- /* 53 */ "\u00B1",
+ /* 56 */ "\u00B1",
// The all letters need to be mirrored are found at
// http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
- /* 54 */ "!fixedColumnOrder!3,<,{,[",
- /* 55 */ "!fixedColumnOrder!3,>,},]",
+ /* 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
- /* 56 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB",
- /* 57 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB",
- /* 58 */ EMPTY,
- /* 59 */ EMPTY,
- /* 60 */ "1",
- /* 61 */ "2",
- /* 62 */ "3",
- /* 63 */ "4",
- /* 64 */ "5",
- /* 65 */ "6",
- /* 66 */ "7",
- /* 67 */ "8",
- /* 68 */ "9",
- /* 69 */ "0",
+ /* 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.
- /* 70 */ "?123",
+ /* 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.
- /* 71 */ "123",
- /* 72~ */
+ /* 74 */ "123",
+ /* 75~ */
EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
- /* ~81 */
+ /* ~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
- /* 82 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B",
+ /* 85 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B",
// U+00B2: "²" SUPERSCRIPT TWO
// U+2154: "⅔" VULGAR FRACTION TWO THIRDS
- /* 83 */ "\u00B2,\u2154",
+ /* 86 */ "\u00B2,\u2154",
// U+00B3: "³" SUPERSCRIPT THREE
// U+00BE: "¾" VULGAR FRACTION THREE QUARTERS
// U+215C: "⅜" VULGAR FRACTION THREE EIGHTHS
- /* 84 */ "\u00B3,\u00BE,\u215C",
+ /* 87 */ "\u00B3,\u00BE,\u215C",
// U+2074: "⁴" SUPERSCRIPT FOUR
- /* 85 */ "\u2074",
+ /* 88 */ "\u2074",
// U+215D: "⅝" VULGAR FRACTION FIVE EIGHTHS
- /* 86 */ "\u215D",
- /* 87 */ EMPTY,
- // U+215E: "⅞" VULGAR FRACTION SEVEN EIGHTHS
- /* 88 */ "\u215E",
- /* 89 */ EMPTY,
+ /* 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
- /* 91 */ "\u207F,\u2205",
- /* 92 */ ",",
- /* 93 */ EMPTY,
- /* 94 */ "?",
- /* 95 */ ";",
- /* 96 */ "%",
+ /* 94 */ "\u207F,\u2205",
+ /* 95 */ ",",
+ /* 96 */ EMPTY,
+ /* 97 */ "?",
+ /* 98 */ ";",
+ /* 99 */ "%",
// U+00A1: "¡" INVERTED EXCLAMATION MARK
- /* 97 */ "\u00A1",
+ /* 100 */ "\u00A1",
// U+00BF: "¿" INVERTED QUESTION MARK
- /* 98 */ "\u00BF",
- /* 99 */ EMPTY,
+ /* 101 */ "\u00BF",
+ /* 102 */ EMPTY,
// U+2030: "‰" PER MILLE SIGN
- /* 100 */ "\u2030",
- /* 101 */ ",",
- /* 102 */ "!",
- /* 103 */ "!",
- /* 104 */ "?",
- /* 105 */ "?",
- /* 106 */ "\'",
- /* 107 */ "\"",
- /* 108 */ "\"",
- /* 109 */ EMPTY,
- /* 110 */ EMPTY,
- /* 111 */ "q",
- /* 112 */ "w",
- /* 113 */ "y",
- /* 114 */ "x",
- // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
- /* 115 */ "\u00F1",
- /* 116 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm",
- /* 117 */ "!icon/settings_key|!code/key_settings",
- /* 118 */ "!icon/shortcut_key|!code/key_shortcut",
- /* 119 */ "!hasLabels!,!text/label_next_key|!code/key_action_next",
- /* 120 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous",
+ /* 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!
- /* 121 */ "= \\ <",
+ /* 124 */ "= \\ <",
// Label for "switch to more symbol" modifier key on tablets. Must be short to fit on key!
- /* 122 */ "~ \\ {",
+ /* 125 */ "~ [ <",
// Label for "Tab" key. Must be short to fit on key!
- /* 123 */ "Tab",
+ /* 126 */ "Tab",
// Label for "switch to phone numeric" key. Must be short to fit on key!
- /* 124 */ "123",
+ /* 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
- /* 125 */ "\uFF0A\uFF03",
+ /* 128 */ "\uFF0A\uFF03",
// Key label for "ante meridiem"
- /* 126 */ "AM",
+ /* 129 */ "AM",
// Key label for "post meridiem"
- /* 127 */ "PM",
- // Label for "switch to symbols" key on PC QWERTY layout
- /* 128 */ "Sym",
- /* 129 */ ".com",
+ /* 130 */ "PM",
+ /* 131 */ ".com",
// popular web domains for the locale - most popular, displayed on the keyboard
- /* 130 */ "!hasLabels!,.net,.org,.gov,.edu",
- /* 131 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ",
+ /* 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
@@ -421,24 +421,24 @@ public final class KeyboardTextsSet {
// 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>.
- /* 132 */ "\u2039,\u203A",
- /* 133 */ "\u2039|\u203A,\u203A|\u2039",
- /* 134 */ "\u203A,\u2039",
- /* 135 */ "\u00AB,\u00BB",
- /* 136 */ "\u00AB|\u00BB,\u00BB|\u00AB",
- /* 137 */ "\u00BB,\u00AB",
+ /* 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>.
- /* 138 */ "\u201A,\u2018,\u2019",
- /* 139 */ "\u2019,\u201A,\u2018",
- /* 140 */ "\u2018,\u201A,\u2019",
- /* 141 */ "\u201E,\u201C,\u201D",
- /* 142 */ "\u201D,\u201E,\u201C",
- /* 143 */ "\u201C,\u201E,\u201D",
- /* 144 */ "!fixedColumnOrder!5,!text/single_quotes,!text/single_angle_quotes",
- /* 145 */ "!fixedColumnOrder!5,!text/double_quotes,!text/double_angle_quotes",
- /* 146 */ "!fixedColumnOrder!6,!text/double_quotes,!text/single_quotes,!text/double_angle_quotes,!text/single_angle_quotes",
+ /* 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",
};
/* Language af: Afrikaans */
@@ -499,45 +499,45 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~41 */
+ 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
- /* 42 */ "\u0623\u200C\u0628\u200C\u062C",
- /* 43 */ null,
- /* 44 */ null,
- /* 45 */ "!text/single_laqm_raqm_rtl",
- /* 46 */ "!text/double_laqm_raqm_rtl",
- /* 47~ */
+ /* 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,
- /* ~49 */
+ /* ~52 */
// U+061F: "؟" ARABIC QUESTION MARK
// U+060C: "،" ARABIC COMMA
// U+061B: "؛" ARABIC SEMICOLON
- /* 50 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
+ /* 53 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(|),)|(",
// U+2605: "★" BLACK STAR
// U+066D: "٭" ARABIC FIVE POINTED STAR
- /* 51 */ "\u2605,\u066D",
+ /* 54 */ "\u2605,\u066D",
// U+266A: "♪" EIGHTH NOTE
- /* 52 */ "\u266A",
- /* 53 */ null,
+ /* 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
- /* 54 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
- /* 55 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[",
+ /* 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
- /* 56 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
- /* 57 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
+ /* 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
@@ -554,70 +554,116 @@ public final class KeyboardTextsSet {
// 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.
- /* 58 */ "!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",
- /* 59 */ "\u0651",
+ /* 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
- /* 60 */ "\u0661",
+ /* 63 */ "\u0661",
// U+0662: "٢" ARABIC-INDIC DIGIT TWO
- /* 61 */ "\u0662",
+ /* 64 */ "\u0662",
// U+0663: "٣" ARABIC-INDIC DIGIT THREE
- /* 62 */ "\u0663",
+ /* 65 */ "\u0663",
// U+0664: "٤" ARABIC-INDIC DIGIT FOUR
- /* 63 */ "\u0664",
+ /* 66 */ "\u0664",
// U+0665: "٥" ARABIC-INDIC DIGIT FIVE
- /* 64 */ "\u0665",
+ /* 67 */ "\u0665",
// U+0666: "٦" ARABIC-INDIC DIGIT SIX
- /* 65 */ "\u0666",
+ /* 68 */ "\u0666",
// U+0667: "٧" ARABIC-INDIC DIGIT SEVEN
- /* 66 */ "\u0667",
+ /* 69 */ "\u0667",
// U+0668: "٨" ARABIC-INDIC DIGIT EIGHT
- /* 67 */ "\u0668",
+ /* 70 */ "\u0668",
// U+0669: "٩" ARABIC-INDIC DIGIT NINE
- /* 68 */ "\u0669",
+ /* 71 */ "\u0669",
// U+0660: "٠" ARABIC-INDIC DIGIT ZERO
- /* 69 */ "\u0660",
+ /* 72 */ "\u0660",
// Label for "switch to symbols" key.
// U+061F: "؟" ARABIC QUESTION MARK
- /* 70 */ "\u0663\u0662\u0661\u061F",
+ /* 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.
- /* 71 */ "\u0663\u0662\u0661",
- /* 72 */ "1",
- /* 73 */ "2",
- /* 74 */ "3",
- /* 75 */ "4",
- /* 76 */ "5",
- /* 77 */ "6",
- /* 78 */ "7",
- /* 79 */ "8",
- /* 80 */ "9",
+ /* 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
- /* 81 */ "0,\u066B,\u066C",
- /* 82~ */
+ /* 84 */ "0,\u066B,\u066C",
+ /* 85~ */
null, null, null, null, null, null, null, null, null, null,
- /* ~91 */
+ /* ~94 */
// U+060C: "،" ARABIC COMMA
- /* 92 */ "\u060C",
- /* 93 */ "\\,",
- /* 94 */ "\u061F",
- /* 95 */ "\u061B",
+ /* 95 */ "\u060C",
+ /* 96 */ "\\,",
+ /* 97 */ "\u061F",
+ /* 98 */ "\u061B",
// U+066A: "٪" ARABIC PERCENT SIGN
- /* 96 */ "\u066A",
- /* 97 */ null,
- /* 98 */ "?",
- /* 99 */ ";",
+ /* 99 */ "\u066A",
+ /* 100 */ null,
+ /* 101 */ "?",
+ /* 102 */ ";",
// U+2030: "‰" PER MILLE SIGN
- /* 100 */ "\\%,\u2030",
- /* 101~ */
+ /* 103 */ "\\%,\u2030",
+ /* 104~ */
null, null, null, null, null,
- /* ~105 */
+ /* ~108 */
// U+060C: "،" ARABIC COMMA
// U+061B: "؛" ARABIC SEMICOLON
// U+061F: "؟" ARABIC QUESTION MARK
- /* 106 */ "\u060C",
- /* 107 */ "\u061F",
- /* 108 */ "\u061F,\u061B,!,:,-,/,\',\"",
+ /* 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 */
@@ -637,23 +683,23 @@ public final class KeyboardTextsSet {
// U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I
/* 29 */ "\u0456",
/* 30~ */
- null, null, null, null, null,
- /* ~34 */
+ null, null, null, null, null, null, null,
+ /* ~36 */
// U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 35 */ "\u044A",
- /* 36~ */
- null, null, null, null,
- /* ~39 */
+ /* 37 */ "\u044A",
+ /* 38~ */
+ null, null, null, null, null,
+ /* ~42 */
// U+0451: "ё" CYRILLIC SMALL LETTER IO
- /* 40 */ "\u0451",
- /* 41 */ null,
+ /* 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
- /* 42 */ "\u0410\u0411\u0412",
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
+ /* 45 */ "\u0410\u0411\u0412",
+ /* 46 */ "!text/single_9qm_lqm",
+ /* 47 */ "!text/double_9qm_lqm",
};
/* Language bg: Bulgarian */
@@ -661,16 +707,16 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~41 */
+ 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
- /* 42 */ "\u0410\u0411\u0412",
- /* 43 */ null,
+ /* 45 */ "\u0410\u0411\u0412",
+ /* 46 */ null,
// single_quotes of Bulgarian is default single_quotes_right_left.
- /* 44 */ "!text/double_9qm_lqm",
+ /* 47 */ "!text/double_9qm_lqm",
};
/* Language ca: Catalan */
@@ -728,9 +774,28 @@ public final class KeyboardTextsSet {
/* 8~ */
null, null, null, null, null, null,
/* ~13 */
- // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT
+ // U+00B7: "·" MIDDLE DOT
// U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
- /* 14 */ "\u0140,\u0142",
+ /* 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!4,\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 */
@@ -804,11 +869,12 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~42 */
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
- /* 45 */ "!text/single_raqm_laqm",
- /* 46 */ "!text/double_raqm_laqm",
+ 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 */
@@ -872,12 +938,12 @@ public final class KeyboardTextsSet {
/* 24 */ "\u00F6",
/* 25~ */
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, null,
- /* ~42 */
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
- /* 45 */ "!text/single_raqm_laqm",
- /* 46 */ "!text/double_raqm_laqm",
+ 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 */
@@ -923,12 +989,12 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~42 */
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
- /* 45 */ "!text/single_raqm_laqm",
- /* 46 */ "!text/double_raqm_laqm",
+ 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 */
@@ -936,13 +1002,13 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~41 */
+ 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
- /* 42 */ "\u0391\u0392\u0393",
+ /* 45 */ "\u0391\u0392\u0393",
};
/* Language en: English */
@@ -1113,20 +1179,21 @@ public final class KeyboardTextsSet {
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- /* ~108 */
- /* 109 */ "q",
- /* 110 */ "x",
+ 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
- /* 111 */ "\u015D",
+ /* 114 */ "\u015D",
// U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX
- /* 112 */ "\u011D",
+ /* 115 */ "\u011D",
// U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE
- /* 113 */ "\u016D",
+ /* 116 */ "\u016D",
// U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX
- /* 114 */ "\u0109",
+ /* 117 */ "\u0109",
// U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX
- /* 115 */ "\u0135",
+ /* 118 */ "\u0135",
};
/* Language es: Spanish */
@@ -1184,25 +1251,30 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~49 */
+ 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
- /* 50 */ "!fixedColumnOrder!9,\u00A1,\",\',#,-,:,!,\\,,?,\u00BF,@,&,\\%,+,;,/,(,)",
- /* 51~ */
+ /* 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,
- /* ~102 */
+ /* ~105 */
// U+00A1: "¡" INVERTED EXCLAMATION MARK
- /* 103 */ "!,\u00A1",
- /* 104 */ null,
+ /* 106 */ "!,\u00A1",
+ /* 107 */ null,
// U+00BF: "¿" INVERTED QUESTION MARK
- /* 105 */ "?,\u00BF",
- /* 106 */ "\"",
- /* 107 */ "\'",
- /* 108 */ "\'",
+ /* 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 */
@@ -1305,10 +1377,10 @@ public final class KeyboardTextsSet {
/* 23 */ "\u00F5",
/* 24~ */
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, null, null,
- /* ~42 */
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
+ null, null, null, null, null, null, null,
+ /* ~45 */
+ /* 46 */ "!text/single_9qm_lqm",
+ /* 47 */ "!text/double_9qm_lqm",
};
/* Language fa: Persian */
@@ -1316,45 +1388,46 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~41 */
+ 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
- /* 42 */ "\u0627\u200C\u0628\u200C\u067E",
- /* 43 */ null,
- /* 44 */ null,
- /* 45 */ "!text/single_laqm_raqm_rtl",
- /* 46 */ "!text/double_laqm_raqm_rtl",
- /* 47~ */
- null, null, null,
- /* ~49 */
+ /* 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
- /* 50 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
+ /* 53 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(|),)|(",
// U+2605: "★" BLACK STAR
// U+066D: "٭" ARABIC FIVE POINTED STAR
- /* 51 */ "\u2605,\u066D",
+ /* 54 */ "\u2605,\u066D",
// U+266A: "♪" EIGHTH NOTE
- /* 52 */ "\u266A",
- /* 53 */ null,
+ /* 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
- /* 54 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
- /* 55 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[",
+ /* 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
- /* 56 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>",
- /* 57 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<",
+ /* 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
@@ -1371,74 +1444,74 @@ public final class KeyboardTextsSet {
// 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.
- /* 58 */ "!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",
- /* 59 */ "\u064B",
+ /* 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
- /* 60 */ "\u06F1",
+ /* 63 */ "\u06F1",
// U+06F2: "۲" EXTENDED ARABIC-INDIC DIGIT TWO
- /* 61 */ "\u06F2",
+ /* 64 */ "\u06F2",
// U+06F3: "۳" EXTENDED ARABIC-INDIC DIGIT THREE
- /* 62 */ "\u06F3",
+ /* 65 */ "\u06F3",
// U+06F4: "۴" EXTENDED ARABIC-INDIC DIGIT FOUR
- /* 63 */ "\u06F4",
+ /* 66 */ "\u06F4",
// U+06F5: "۵" EXTENDED ARABIC-INDIC DIGIT FIVE
- /* 64 */ "\u06F5",
+ /* 67 */ "\u06F5",
// U+06F6: "۶" EXTENDED ARABIC-INDIC DIGIT SIX
- /* 65 */ "\u06F6",
+ /* 68 */ "\u06F6",
// U+06F7: "۷" EXTENDED ARABIC-INDIC DIGIT SEVEN
- /* 66 */ "\u06F7",
+ /* 69 */ "\u06F7",
// U+06F8: "۸" EXTENDED ARABIC-INDIC DIGIT EIGHT
- /* 67 */ "\u06F8",
+ /* 70 */ "\u06F8",
// U+06F9: "۹" EXTENDED ARABIC-INDIC DIGIT NINE
- /* 68 */ "\u06F9",
+ /* 71 */ "\u06F9",
// U+06F0: "۰" EXTENDED ARABIC-INDIC DIGIT ZERO
- /* 69 */ "\u06F0",
+ /* 72 */ "\u06F0",
// Label for "switch to symbols" key.
// U+061F: "؟" ARABIC QUESTION MARK
- /* 70 */ "\u06F3\u06F2\u06F1\u061F",
+ /* 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.
- /* 71 */ "\u06F3\u06F2\u06F1",
- /* 72 */ "1",
- /* 73 */ "2",
- /* 74 */ "3",
- /* 75 */ "4",
- /* 76 */ "5",
- /* 77 */ "6",
- /* 78 */ "7",
- /* 79 */ "8",
- /* 80 */ "9",
+ /* 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
- /* 81 */ "0,\u066B,\u066C",
- /* 82~ */
+ /* 84 */ "0,\u066B,\u066C",
+ /* 85~ */
null, null, null, null, null, null, null, null, null, null,
- /* ~91 */
+ /* ~94 */
// U+060C: "،" ARABIC COMMA
- /* 92 */ "\u060C",
- /* 93 */ "\\,",
- /* 94 */ "\u061F",
- /* 95 */ "\u061B",
+ /* 95 */ "\u060C",
+ /* 96 */ "\\,",
+ /* 97 */ "\u061F",
+ /* 98 */ "\u061B",
// U+066A: "٪" ARABIC PERCENT SIGN
- /* 96 */ "\u066A",
- /* 97 */ null,
- /* 98 */ "?",
- /* 99 */ ";",
+ /* 99 */ "\u066A",
+ /* 100 */ null,
+ /* 101 */ "?",
+ /* 102 */ ";",
// U+2030: "‰" PER MILLE SIGN
- /* 100 */ "\\%,\u2030",
+ /* 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
- /* 101 */ "\u060C",
- /* 102 */ "!",
- /* 103 */ "!,\\,",
- /* 104 */ "\u061F",
- /* 105 */ "\u061F,?",
- /* 106 */ "\u060C",
+ /* 104 */ "\u060C",
+ /* 105 */ "!",
+ /* 106 */ "!,\\,",
/* 107 */ "\u061F",
- /* 108 */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,/,\u00AB|\u00BB,\u00BB|\u00AB",
+ /* 108 */ "\u061F,?",
+ /* 109 */ "\u060C",
+ /* 110 */ "\u061F",
+ /* 111 */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,/,\u00AB|\u00BB,\u00BB|\u00AB",
};
/* Language fi: Finnish */
@@ -1546,56 +1619,56 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~41 */
+ 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
- /* 42 */ "\u0915\u0916\u0917",
- /* 43~ */
+ /* 45 */ "\u0915\u0916\u0917",
+ /* 46~ */
null, null, null, null, null,
- /* ~47 */
+ /* ~50 */
// U+20B9: "₹" INDIAN RUPEE SIGN
- /* 48 */ "\u20B9",
- /* 49~ */
+ /* 51 */ "\u20B9",
+ /* 52~ */
null, null, null, null, null, null, null, null, null, null, null,
- /* ~59 */
+ /* ~62 */
// U+0967: "१" DEVANAGARI DIGIT ONE
- /* 60 */ "\u0967",
+ /* 63 */ "\u0967",
// U+0968: "२" DEVANAGARI DIGIT TWO
- /* 61 */ "\u0968",
+ /* 64 */ "\u0968",
// U+0969: "३" DEVANAGARI DIGIT THREE
- /* 62 */ "\u0969",
+ /* 65 */ "\u0969",
// U+096A: "४" DEVANAGARI DIGIT FOUR
- /* 63 */ "\u096A",
+ /* 66 */ "\u096A",
// U+096B: "५" DEVANAGARI DIGIT FIVE
- /* 64 */ "\u096B",
+ /* 67 */ "\u096B",
// U+096C: "६" DEVANAGARI DIGIT SIX
- /* 65 */ "\u096C",
+ /* 68 */ "\u096C",
// U+096D: "७" DEVANAGARI DIGIT SEVEN
- /* 66 */ "\u096D",
+ /* 69 */ "\u096D",
// U+096E: "८" DEVANAGARI DIGIT EIGHT
- /* 67 */ "\u096E",
+ /* 70 */ "\u096E",
// U+096F: "९" DEVANAGARI DIGIT NINE
- /* 68 */ "\u096F",
+ /* 71 */ "\u096F",
// U+0966: "०" DEVANAGARI DIGIT ZERO
- /* 69 */ "\u0966",
+ /* 72 */ "\u0966",
// Label for "switch to symbols" key.
- /* 70 */ "?\u0967\u0968\u0969",
+ /* 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.
- /* 71 */ "\u0967\u0968\u0969",
- /* 72 */ "1",
- /* 73 */ "2",
- /* 74 */ "3",
- /* 75 */ "4",
- /* 76 */ "5",
- /* 77 */ "6",
- /* 78 */ "7",
- /* 79 */ "8",
- /* 80 */ "9",
- /* 81 */ "0",
+ /* 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 */
@@ -1626,11 +1699,12 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~42 */
- /* 43 */ "!text/single_9qm_rqm",
- /* 44 */ "!text/double_9qm_rqm",
- /* 45 */ "!text/single_raqm_laqm",
- /* 46 */ "!text/double_raqm_laqm",
+ 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 */
@@ -1679,12 +1753,43 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~42 */
- /* 43 */ "!text/single_9qm_rqm",
- /* 44 */ "!text/double_9qm_rqm",
- /* 45 */ "!text/single_raqm_laqm",
- /* 46 */ "!text/double_raqm_laqm",
+ /* ~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 */
@@ -1750,10 +1855,10 @@ public final class KeyboardTextsSet {
/* 22 */ "\u00FE",
/* 23~ */
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, null, null, null,
- /* ~42 */
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
+ null, null, null, null, null, null, null, null,
+ /* ~45 */
+ /* 46 */ "!text/single_9qm_lqm",
+ /* 47 */ "!text/double_9qm_lqm",
};
/* Language it: Italian */
@@ -1806,13 +1911,13 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~41 */
+ 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
- /* 42 */ "\u05D0\u05D1\u05D2",
+ /* 45 */ "\u05D0\u05D1\u05D2",
// The following characters don't need BIDI mirroring.
// U+2018: "‘" LEFT SINGLE QUOTATION MARK
// U+2019: "’" RIGHT SINGLE QUOTATION MARK
@@ -1820,31 +1925,42 @@ public final class KeyboardTextsSet {
// U+201C: "“" LEFT DOUBLE QUOTATION MARK
// U+201D: "”" RIGHT DOUBLE QUOTATION MARK
// U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
- /* 43 */ "\u2018,\u2019,\u201A",
- /* 44 */ "\u201C,\u201D,\u201E",
- /* 45 */ "!text/single_laqm_raqm_rtl",
- /* 46 */ "!text/double_laqm_raqm_rtl",
- /* 47~ */
- null, null, null, null,
- /* ~50 */
- // U+2605: "★" BLACK STAR
- /* 51 */ "\u2605",
+ /* 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 */ null,
+ // U+2605: "★" BLACK STAR
+ /* 54 */ "\u2605",
+ /* 55 */ null,
// U+00B1: "±" PLUS-MINUS SIGN
// U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN
- /* 53 */ "\u00B1,\uFB29",
+ /* 56 */ "\u00B1,\uFB29",
// The all letters need to be mirrored are found at
// http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
- /* 54 */ "!fixedColumnOrder!3,<|>,{|},[|]",
- /* 55 */ "!fixedColumnOrder!3,>|<,}|{,]|[",
+ /* 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
- /* 56 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
- /* 57 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
+ /* 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 */
@@ -1852,15 +1968,82 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~41 */
+ 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
- /* 42 */ "\u10D0\u10D1\u10D2",
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
+ /* 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 */
@@ -1881,25 +2064,46 @@ public final class KeyboardTextsSet {
/* 29 */ "\u0438",
// U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U
/* 30 */ "\u04AF",
+ /* 31 */ null,
// U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER
- /* 31 */ "\u04A3",
- /* 32 */ null,
- /* 33 */ null,
+ /* 32 */ "\u04A3",
+ /* 33~ */
+ null, null, null,
+ /* ~35 */
// U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O
- /* 34 */ "\u04E9",
+ /* 36 */ "\u04E9",
// U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 35 */ "\u044A",
- /* 36~ */
- null, null, null, null,
- /* ~39 */
+ /* 37 */ "\u044A",
+ /* 38~ */
+ null, null, null, null, null,
+ /* ~42 */
// U+0451: "ё" CYRILLIC SMALL LETTER IO
- /* 40 */ "\u0451",
- /* 41 */ null,
+ /* 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
- /* 42 */ "\u0410\u0411\u0412",
+ /* 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 */
@@ -1992,10 +2196,10 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~42 */
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
+ 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 */
@@ -2087,10 +2291,10 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~42 */
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
+ 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 */
@@ -2098,27 +2302,27 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~35 */
+ null, null, null, null, null, null, null, null, null,
+ /* ~38 */
// U+0455: "ѕ" CYRILLIC SMALL LETTER DZE
- /* 36 */ "\u0455",
+ /* 39 */ "\u0455",
// U+045C: "ќ" CYRILLIC SMALL LETTER KJE
- /* 37 */ "\u045C",
+ /* 40 */ "\u045C",
// U+0437: "з" CYRILLIC SMALL LETTER ZE
- /* 38 */ "\u0437",
+ /* 41 */ "\u0437",
// U+0453: "ѓ" CYRILLIC SMALL LETTER GJE
- /* 39 */ "\u0453",
+ /* 42 */ "\u0453",
// U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE
- /* 40 */ "\u0450",
+ /* 43 */ "\u0450",
// U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE
- /* 41 */ "\u045D",
+ /* 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
- /* 42 */ "\u0410\u0411\u0412",
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
+ /* 45 */ "\u0410\u0411\u0412",
+ /* 46 */ "!text/single_9qm_lqm",
+ /* 47 */ "!text/double_9qm_lqm",
};
/* Language mn: Mongolian */
@@ -2126,18 +2330,18 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~41 */
+ 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
- /* 42 */ "\u0410\u0411\u0412",
- /* 43~ */
+ /* 45 */ "\u0410\u0411\u0412",
+ /* 46~ */
null, null, null, null, null,
- /* ~47 */
+ /* ~50 */
// U+20AE: "₮" TUGRIK SIGN
- /* 48 */ "\u20AE",
+ /* 51 */ "\u20AE",
};
/* Language nb: Norwegian Bokmål */
@@ -2187,10 +2391,67 @@ public final class KeyboardTextsSet {
/* 24 */ "\u00E4",
/* 25~ */
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, null,
- /* ~42 */
- /* 43 */ "!text/single_9qm_rqm",
- /* 44 */ "!text/double_9qm_rqm",
+ 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 */
@@ -2245,10 +2506,10 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~42 */
- /* 43 */ "!text/single_9qm_rqm",
- /* 44 */ "!text/double_9qm_rqm",
+ null, null, null, null, null, null, null,
+ /* ~45 */
+ /* 46 */ "!text/single_9qm_rqm",
+ /* 47 */ "!text/double_9qm_rqm",
};
/* Language pl: Polish */
@@ -2305,10 +2566,11 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~42 */
- /* 43 */ "!text/single_9qm_rqm",
- /* 44 */ "!text/double_9qm_rqm",
+ 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 */
@@ -2411,10 +2673,10 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~42 */
- /* 43 */ "!text/single_9qm_rqm",
- /* 44 */ "!text/double_9qm_rqm",
+ null, null, null, null,
+ /* ~45 */
+ /* 46 */ "!text/single_9qm_rqm",
+ /* 47 */ "!text/double_9qm_rqm",
};
/* Language ru: Russian */
@@ -2434,23 +2696,23 @@ public final class KeyboardTextsSet {
// U+0438: "и" CYRILLIC SMALL LETTER I
/* 29 */ "\u0438",
/* 30~ */
- null, null, null, null, null,
- /* ~34 */
+ null, null, null, null, null, null, null,
+ /* ~36 */
// U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 35 */ "\u044A",
- /* 36~ */
- null, null, null, null,
- /* ~39 */
+ /* 37 */ "\u044A",
+ /* 38~ */
+ null, null, null, null, null,
+ /* ~42 */
// U+0451: "ё" CYRILLIC SMALL LETTER IO
- /* 40 */ "\u0451",
- /* 41 */ null,
+ /* 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
- /* 42 */ "\u0410\u0411\u0412",
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
+ /* 45 */ "\u0410\u0411\u0412",
+ /* 46 */ "!text/single_9qm_lqm",
+ /* 47 */ "!text/double_9qm_lqm",
};
/* Language sk: Slovak */
@@ -2543,12 +2805,12 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~42 */
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
- /* 45 */ "!text/single_raqm_laqm",
- /* 46 */ "!text/double_raqm_laqm",
+ 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 */
@@ -2572,11 +2834,12 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~42 */
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
- /* 45 */ "!text/single_raqm_laqm",
- /* 46 */ "!text/double_raqm_laqm",
+ 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 */
@@ -2584,8 +2847,8 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~35 */
+ 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
@@ -2605,27 +2868,27 @@ public final class KeyboardTextsSet {
// END: More keys definitions for Serbian (Latin)
// BEGIN: More keys definitions for Serbian (Cyrillic)
// U+0437: "з" CYRILLIC SMALL LETTER ZE
- /* 36 */ "\u0437",
+ /* 39 */ "\u0437",
// U+045B: "ћ" CYRILLIC SMALL LETTER TSHE
- /* 37 */ "\u045B",
+ /* 40 */ "\u045B",
// U+0455: "ѕ" CYRILLIC SMALL LETTER DZE
- /* 38 */ "\u0455",
+ /* 41 */ "\u0455",
// U+0452: "ђ" CYRILLIC SMALL LETTER DJE
- /* 39 */ "\u0452",
+ /* 42 */ "\u0452",
// U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE
- /* 40 */ "\u0450",
+ /* 43 */ "\u0450",
// U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE
- /* 41 */ "\u045D",
+ /* 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
- /* 42 */ "\u0410\u0411\u0412",
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
- /* 45 */ "!text/single_raqm_laqm",
- /* 46 */ "!text/double_raqm_laqm",
+ /* 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 */
@@ -2670,10 +2933,10 @@ public final class KeyboardTextsSet {
/* 24 */ "\u00E6",
/* 25~ */
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, null, null, null,
- /* ~44 */
- /* 45 */ "!text/single_raqm_laqm",
- /* 46 */ "!text/double_raqm_laqm",
+ null, null, null, null, null, null, null, null,
+ /* ~47 */
+ /* 48 */ "!text/single_raqm_laqm",
+ /* 49 */ "!text/double_raqm_laqm",
};
/* Language sw: Swahili */
@@ -2732,18 +2995,18 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~41 */
+ 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
- /* 42 */ "\u0E01\u0E02\u0E04",
- /* 43~ */
+ /* 45 */ "\u0E01\u0E02\u0E04",
+ /* 46~ */
null, null, null, null, null,
- /* ~47 */
+ /* ~50 */
// U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT
- /* 48 */ "\u0E3F",
+ /* 51 */ "\u0E3F",
};
/* Language tl: Tagalog */
@@ -2861,30 +3124,32 @@ public final class KeyboardTextsSet {
/* 28 */ "\u0454",
// U+0438: "и" CYRILLIC SMALL LETTER I
/* 29 */ "\u0438",
- /* 30 */ null,
- /* 31 */ null,
+ /* 30~ */
+ null, null, null,
+ /* ~32 */
// U+0491: "ґ" CYRILLIC SMALL LETTER GHE WITH UPTURN
- /* 32 */ "\u0491",
+ /* 33 */ "\u0491",
// U+0457: "ї" CYRILLIC SMALL LETTER YI
- /* 33 */ "\u0457",
- /* 34 */ null,
+ /* 34 */ "\u0457",
+ /* 35 */ null,
+ /* 36 */ null,
// U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 35 */ "\u044A",
- /* 36~ */
- null, null, null, null, null, null,
- /* ~41 */
+ /* 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
- /* 42 */ "\u0410\u0411\u0412",
- /* 43 */ "!text/single_9qm_lqm",
- /* 44 */ "!text/double_9qm_lqm",
- /* 45~ */
+ /* 45 */ "\u0410\u0411\u0412",
+ /* 46 */ "!text/single_9qm_lqm",
+ /* 47 */ "!text/double_9qm_lqm",
+ /* 48~ */
null, null, null,
- /* ~47 */
+ /* ~50 */
// U+20B4: "₴" HRYVNIA SIGN
- /* 48 */ "\u20B4",
+ /* 51 */ "\u20B4",
};
/* Language vi: Vietnamese */
@@ -2969,10 +3234,10 @@ public final class KeyboardTextsSet {
/* 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,
- /* ~47 */
+ null, null, null, null, null, null, null, null, null, null, null,
+ /* ~50 */
// U+20AB: "₫" DONG SIGN
- /* 48 */ "\u20AB",
+ /* 51 */ "\u20AB",
};
/* Language zu: Zulu */
@@ -3022,7 +3287,7 @@ public final class KeyboardTextsSet {
/* 7 */ "\u00E7",
};
- /* Language zz: No language */
+ /* 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
@@ -3149,6 +3414,7 @@ public final class KeyboardTextsSet {
"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 */
@@ -3166,16 +3432,21 @@ public final class KeyboardTextsSet {
"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 */
@@ -3193,7 +3464,7 @@ public final class KeyboardTextsSet {
"uk", LANGUAGE_uk, /* Ukrainian */
"vi", LANGUAGE_vi, /* Vietnamese */
"zu", LANGUAGE_zu, /* Zulu */
- "zz", LANGUAGE_zz, /* No language */
+ "zz", LANGUAGE_zz, /* Alphabet */
};
static {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java
index db154a391..7c2e3e174 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java
@@ -17,7 +17,7 @@
package com.android.inputmethod.keyboard.internal;
import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.HashMap;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java b/java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java
index 4916a15b5..c1f374964 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java
@@ -30,7 +30,6 @@ import java.util.Arrays;
public class MatrixUtils {
private static final String TAG = MatrixUtils.class.getSimpleName();
public static class MatrixOperationFailedException extends Exception {
- private static final String TAG = MatrixOperationFailedException.class.getSimpleName();
private static final long serialVersionUID = 4384485606788583829L;
public MatrixOperationFailedException(String msg) {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
index b38d79f7b..110936f8f 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
@@ -19,7 +19,7 @@ package com.android.inputmethod.keyboard.internal;
import android.text.TextUtils;
import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.StringUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
import java.util.Locale;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java b/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java
new file mode 100644
index 000000000..a0935b985
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java
@@ -0,0 +1,113 @@
+/*
+ * 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.util.Log;
+import android.view.MotionEvent;
+
+import com.android.inputmethod.keyboard.Key;
+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 int mOldPointerCount = 1;
+ private Key mOldKey;
+ private int[] mLastCoords = CoordinateUtils.newInstance();
+
+ public void processMotionEvent(final MotionEvent me, final KeyEventHandler keyEventHandler) {
+ final int pointerCount = me.getPointerCount();
+ final int oldPointerCount = mOldPointerCount;
+ mOldPointerCount = pointerCount;
+ // Ignore continuous multi-touch events because we can't trust the coordinates
+ // in multi-touch events.
+ if (pointerCount > 1 && oldPointerCount > 1) {
+ return;
+ }
+
+ // Use only main (id=0) pointer tracker.
+ final PointerTracker mainTracker = PointerTracker.getPointerTracker(0, keyEventHandler);
+ final int action = me.getActionMasked();
+ final int index = me.getActionIndex();
+ final long eventTime = me.getEventTime();
+ final long downTime = me.getDownTime();
+
+ // In single-touch.
+ if (oldPointerCount == 1 && pointerCount == 1) {
+ if (me.getPointerId(index) == mainTracker.mPointerId) {
+ mainTracker.processMotionEvent(me, keyEventHandler);
+ return;
+ }
+ // Inject a copied event.
+ injectMotionEvent(action, me.getX(index), me.getY(index), downTime, eventTime,
+ mainTracker, keyEventHandler);
+ return;
+ }
+
+ // Single-touch to multi-touch transition.
+ if (oldPointerCount == 1 && pointerCount == 2) {
+ // Send an up event for the last pointer, be cause we can't trust the coordinates of
+ // this multi-touch event.
+ mainTracker.getLastCoordinates(mLastCoords);
+ final int x = CoordinateUtils.x(mLastCoords);
+ final int y = CoordinateUtils.y(mLastCoords);
+ mOldKey = mainTracker.getKeyOn(x, y);
+ // Inject an artifact up event for the old key.
+ injectMotionEvent(MotionEvent.ACTION_UP, x, y, downTime, eventTime,
+ mainTracker, keyEventHandler);
+ return;
+ }
+
+ // Multi-touch to single-touch transition.
+ if (oldPointerCount == 2 && pointerCount == 1) {
+ // Send a down event for the latest pointer if the key is different from the previous
+ // key.
+ final int x = (int)me.getX(index);
+ final int y = (int)me.getY(index);
+ final Key newKey = mainTracker.getKeyOn(x, y);
+ if (mOldKey != newKey) {
+ // 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);
+ 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);
+ }
+ }
+ return;
+ }
+
+ Log.w(TAG, "Unknown touch panel behavior: pointer count is "
+ + pointerCount + " (previously " + oldPointerCount + ")");
+ }
+
+ 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 MotionEvent me = MotionEvent.obtain(
+ downTime, eventTime, action, x, y, 0 /* metaState */);
+ try {
+ tracker.processMotionEvent(me, handler);
+ } 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 31ef3cd8f..7ee45e8f6 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
@@ -18,7 +18,7 @@ package com.android.inputmethod.keyboard.internal;
import android.util.Log;
-import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.ArrayList;
@@ -207,7 +207,7 @@ public final class PointerTrackerQueue {
}
}
- public void cancelAllPointerTracker() {
+ public void cancelAllPointerTrackers() {
synchronized (mExpandableArrayOfActivePointers) {
if (DEBUG) {
Log.d(TAG, "cancelAllPointerTracker: " + this);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
index 23761103f..4c8607da8 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
@@ -24,8 +24,8 @@ import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.widget.RelativeLayout;
-import com.android.inputmethod.latin.CollectionUtils;
-import com.android.inputmethod.latin.CoordinateUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.CoordinateUtils;
import java.util.ArrayList;
@@ -37,7 +37,10 @@ public final class PreviewPlacerView extends RelativeLayout {
public PreviewPlacerView(final Context context, final AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
+ }
+ public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
+ if (!enabled) return;
final Paint layerPaint = new Paint();
layerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
setLayerType(LAYER_TYPE_HARDWARE, layerPaint);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/RoundedLine.java b/java/src/com/android/inputmethod/keyboard/internal/RoundedLine.java
index 2eefd6ae9..211ef5f8b 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/RoundedLine.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/RoundedLine.java
@@ -37,16 +37,18 @@ public final class RoundedLine {
* @param p2x the x-coordinate of the end point.
* @param p2y the y-coordinate of the end point.
* @param r2 the radius at the end point
- * @return the path of rounded line
+ * @return an instance of {@link Path} that holds the result rounded line, or an instance of
+ * {@link Path} that holds an empty path if the start and end points are equal.
*/
public Path makePath(final float p1x, final float p1y, final float r1,
final float p2x, final float p2y, final float r2) {
+ mPath.rewind();
final double dx = p2x - p1x;
final double dy = p2y - p1y;
// Distance of the points.
final double l = Math.hypot(dx, dy);
if (Double.compare(0.0d, l) == 0) {
- return null;
+ return mPath; // Return an empty path
}
// Angle of the line p1-p2
final double a = Math.atan2(dy, dx);
@@ -86,7 +88,6 @@ public final class RoundedLine {
mArc2.set(p2x, p2y, p2x, p2y);
mArc2.inset(-r2, -r2);
- mPath.rewind();
// Trail cap at P1.
mPath.moveTo(p1x, p1y);
mPath.arcTo(mArc1, angle, a1);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java b/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java
new file mode 100644
index 000000000..b8ee976e8
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/ScrollKeyboardView.java
@@ -0,0 +1,212 @@
+/*
+ * 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.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.latin.R;
+
+/**
+ * This is an extended {@link KeyboardView} class that hosts a scroll keyboard.
+ * Multi-touch unsupported. No {@link PointerTracker}s. No gesture support.
+ */
+// TODO: Implement key popup preview.
+public final class ScrollKeyboardView extends KeyboardView implements
+ ScrollViewWithNotifier.ScrollListener, GestureDetector.OnGestureListener {
+ private static final boolean PAGINATION = false;
+
+ public interface OnKeyClickListener {
+ public void onKeyClick(Key key);
+ }
+
+ private static final OnKeyClickListener EMPTY_LISTENER = new OnKeyClickListener() {
+ @Override
+ public void onKeyClick(final Key key) {}
+ };
+
+ private OnKeyClickListener mListener = EMPTY_LISTENER;
+ private final KeyDetector mKeyDetector = new KeyDetector(0.0f /*keyHysteresisDistance */);
+ private final GestureDetector mGestureDetector;
+
+ private final Scroller mScroller;
+ private ScrollViewWithNotifier mScrollView;
+
+ public ScrollKeyboardView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, R.attr.keyboardViewStyle);
+ }
+
+ public ScrollKeyboardView(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();
+ }
+
+ public void setOnKeyClickListener(final OnKeyClickListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setKeyboard(final Keyboard keyboard) {
+ super.setKeyboard(keyboard);
+ mKeyDetector.setKeyboard(keyboard, 0 /* correctionX */, 0 /* correctionY */);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouchEvent(final MotionEvent e) {
+ if (mGestureDetector.onTouchEvent(e)) {
+ return true;
+ }
+ final Key key = getKey(e);
+ if (key != null && key != mCurrentKey) {
+ releaseCurrentKey();
+ }
+ return true;
+ }
+
+ // {@link GestureDetector#OnGestureListener} methods.
+ private Key mCurrentKey;
+
+ private Key getKey(final MotionEvent e) {
+ final int index = e.getActionIndex();
+ final int x = (int)e.getX(index);
+ final int y = (int)e.getY(index);
+ return mKeyDetector.detectHitKey(x, y);
+ }
+
+ public void releaseCurrentKey() {
+ final Key currentKey = mCurrentKey;
+ if (currentKey == null) {
+ return;
+ }
+ currentKey.onReleased();
+ invalidateKey(currentKey);
+ mCurrentKey = null;
+ }
+
+ @Override
+ public boolean onDown(final MotionEvent e) {
+ final Key key = getKey(e);
+ releaseCurrentKey();
+ mCurrentKey = key;
+ if (key == null) {
+ return false;
+ }
+ // TODO: May call {@link KeyboardActionListener#onPressKey(int,int,boolean)}.
+ key.onPressed();
+ invalidateKey(key);
+ return false;
+ }
+
+ @Override
+ public void onShowPress(final MotionEvent e) {
+ // User feedback is done at {@link #onDown(MotionEvent)}.
+ }
+
+ @Override
+ public boolean onSingleTapUp(final MotionEvent e) {
+ final Key key = getKey(e);
+ releaseCurrentKey();
+ if (key == null) {
+ return false;
+ }
+ // TODO: May call {@link KeyboardActionListener#onReleaseKey(int,boolean)}.
+ key.onReleased();
+ invalidateKey(key);
+ mListener.onKeyClick(key);
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(final MotionEvent e1, final MotionEvent e2, final float distanceX,
+ final float distanceY) {
+ releaseCurrentKey();
+ return false;
+ }
+
+ @Override
+ public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX,
+ final float velocityY) {
+ releaseCurrentKey();
+ return false;
+ }
+
+ @Override
+ public void onLongPress(final MotionEvent e) {
+ // Long press detection of {@link #mGestureDetector} is disabled and not used.
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java b/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java
new file mode 100644
index 000000000..d1ccdc7b5
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/ScrollViewWithNotifier.java
@@ -0,0 +1,66 @@
+/*
+ * 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/SlidingKeyInputPreview.java
index 33dbbafa3..2787ebfb9 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputPreview.java
@@ -23,8 +23,8 @@ import android.graphics.Path;
import android.view.View;
import com.android.inputmethod.keyboard.PointerTracker;
-import com.android.inputmethod.latin.CoordinateUtils;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.CoordinateUtils;
/**
* Draw rubber band preview graphics during sliding key input.
@@ -32,7 +32,7 @@ import com.android.inputmethod.latin.R;
public final class SlidingKeyInputPreview extends AbstractDrawingPreview {
private final float mPreviewBodyRadius;
- private boolean mShowSlidingKeyInputPreview;
+ private boolean mShowsSlidingKeyInputPreview;
private final int[] mPreviewFrom = CoordinateUtils.newInstance();
private final int[] mPreviewTo = CoordinateUtils.newInstance();
@@ -62,7 +62,7 @@ public final class SlidingKeyInputPreview extends AbstractDrawingPreview {
}
public void dismissSlidingKeyInputPreview() {
- mShowSlidingKeyInputPreview = false;
+ mShowsSlidingKeyInputPreview = false;
getDrawingView().invalidate();
}
@@ -72,7 +72,7 @@ public final class SlidingKeyInputPreview extends AbstractDrawingPreview {
*/
@Override
public void drawPreview(final Canvas canvas) {
- if (!isPreviewEnabled() || !mShowSlidingKeyInputPreview) {
+ if (!isPreviewEnabled() || !mShowsSlidingKeyInputPreview) {
return;
}
@@ -90,13 +90,9 @@ public final class SlidingKeyInputPreview extends AbstractDrawingPreview {
*/
@Override
public void setPreviewPosition(final PointerTracker tracker) {
- if (!tracker.isInSlidingKeyInputFromModifier()) {
- mShowSlidingKeyInputPreview = false;
- return;
- }
tracker.getDownCoordinates(mPreviewFrom);
tracker.getLastCoordinates(mPreviewTo);
- mShowSlidingKeyInputPreview = true;
+ mShowsSlidingKeyInputPreview = true;
getDrawingView().invalidate();
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java b/java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java
index e5665bcdd..10847f62d 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java
@@ -62,7 +62,7 @@ public class SmoothingUtils {
for (int j = 0; j < COEFF_COUNT; ++j) {
final int pow = i + j;
for (int k = 0; k < N; ++k) {
- m0[i][j] += (float) Math.pow((double) xs[k], pow);
+ m0[i][j] += (float) Math.pow(xs[k], pow);
}
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/TouchScreenRegulator.java b/java/src/com/android/inputmethod/keyboard/internal/TouchScreenRegulator.java
deleted file mode 100644
index d6b1cc6fc..000000000
--- a/java/src/com/android/inputmethod/keyboard/internal/TouchScreenRegulator.java
+++ /dev/null
@@ -1,155 +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.keyboard.internal;
-
-import android.content.Context;
-import android.util.Log;
-import android.view.MotionEvent;
-
-import com.android.inputmethod.keyboard.MainKeyboardView;
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResourceUtils;
-import com.android.inputmethod.latin.define.ProductionFlag;
-import com.android.inputmethod.research.ResearchLogger;
-
-public final class TouchScreenRegulator {
- private static final String TAG = TouchScreenRegulator.class.getSimpleName();
- private static boolean DEBUG_MODE = LatinImeLogger.sDBG;
-
- public interface ProcessMotionEvent {
- public boolean processMotionEvent(MotionEvent me);
- }
-
- private final ProcessMotionEvent mView;
- private final boolean mNeedsSuddenJumpingHack;
-
- /** Whether we've started dropping move events because we found a big jump */
- private boolean mDroppingEvents;
- /**
- * Whether multi-touch disambiguation needs to be disabled if a real multi-touch event has
- * occured
- */
- private boolean mDisableDisambiguation;
- /** The distance threshold at which we start treating the touch session as a multi-touch */
- private int mJumpThresholdSquare = Integer.MAX_VALUE;
- private int mLastX;
- private int mLastY;
- // One-seventh of the keyboard width seems like a reasonable threshold
- private static final float JUMP_THRESHOLD_RATIO_TO_KEYBOARD_WIDTH = 1.0f / 7.0f;
-
- public TouchScreenRegulator(final Context context, final ProcessMotionEvent view) {
- mView = view;
- mNeedsSuddenJumpingHack = Boolean.parseBoolean(ResourceUtils.getDeviceOverrideValue(
- context.getResources(), R.array.sudden_jumping_touch_event_device_list));
- }
-
- public void setKeyboardGeometry(final int keyboardWidth) {
- final float jumpThreshold = keyboardWidth * JUMP_THRESHOLD_RATIO_TO_KEYBOARD_WIDTH;
- mJumpThresholdSquare = (int)(jumpThreshold * jumpThreshold);
- }
-
- /**
- * This function checks to see if we need to handle any sudden jumps in the pointer location
- * that could be due to a multi-touch being treated as a move by the firmware or hardware.
- * Once a sudden jump is detected, all subsequent move events are discarded
- * until an UP is received.<P>
- * When a sudden jump is detected, an UP event is simulated at the last position and when
- * the sudden moves subside, a DOWN event is simulated for the second key.
- * @param me the motion event
- * @return true if the event was consumed, so that it doesn't continue to be handled by
- * {@link MainKeyboardView}.
- */
- private boolean handleSuddenJumping(final MotionEvent me) {
- if (!mNeedsSuddenJumpingHack)
- return false;
- final int action = me.getAction();
- final int x = (int) me.getX();
- final int y = (int) me.getY();
- boolean result = false;
-
- // Real multi-touch event? Stop looking for sudden jumps
- if (me.getPointerCount() > 1) {
- mDisableDisambiguation = true;
- }
- if (mDisableDisambiguation) {
- // If UP, reset the multi-touch flag
- if (action == MotionEvent.ACTION_UP) mDisableDisambiguation = false;
- return false;
- }
-
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- // Reset the "session"
- mDroppingEvents = false;
- mDisableDisambiguation = false;
- break;
- case MotionEvent.ACTION_MOVE:
- // Is this a big jump?
- final int distanceSquare = (mLastX - x) * (mLastX - x) + (mLastY - y) * (mLastY - y);
- // Check the distance.
- if (distanceSquare > mJumpThresholdSquare) {
- // If we're not yet dropping events, start dropping and send an UP event
- if (!mDroppingEvents) {
- mDroppingEvents = true;
- // Send an up event
- MotionEvent translated = MotionEvent.obtain(
- me.getEventTime(), me.getEventTime(),
- MotionEvent.ACTION_UP,
- mLastX, mLastY, me.getMetaState());
- mView.processMotionEvent(translated);
- translated.recycle();
- }
- result = true;
- } else if (mDroppingEvents) {
- // If moves are small and we're already dropping events, continue dropping
- result = true;
- }
- break;
- case MotionEvent.ACTION_UP:
- if (mDroppingEvents) {
- // Send a down event first, as we dropped a bunch of sudden jumps and assume that
- // the user is releasing the touch on the second key.
- MotionEvent translated = MotionEvent.obtain(me.getEventTime(), me.getEventTime(),
- MotionEvent.ACTION_DOWN,
- x, y, me.getMetaState());
- mView.processMotionEvent(translated);
- translated.recycle();
- mDroppingEvents = false;
- // Let the up event get processed as well, result = false
- }
- break;
- }
- // Track the previous coordinate
- mLastX = x;
- mLastY = y;
- return result;
- }
-
- public boolean onTouchEvent(final MotionEvent me) {
- // If there was a sudden jump, return without processing the actual motion event.
- if (handleSuddenJumping(me)) {
- if (DEBUG_MODE)
- Log.w(TAG, "onTouchEvent: ignore sudden jump " + me);
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.suddenJumpingTouchEventHandler_onTouchEvent(me);
- }
- return true;
- }
- return mView.processMotionEvent(me);
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
new file mode 100644
index 000000000..845a9b987
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/AbstractDictionaryWriter.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+// 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();
+
+ abstract public void addUnigramWord(final String word, final String shortcutTarget,
+ final int frequency, 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)
+ throws IOException, UnsupportedFormatException;
+
+ public void write(final String fileName) {
+ 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);
+ 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/AudioAndHapticFeedbackManager.java b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
index 986b1a178..54bc29559 100644
--- a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
+++ b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
@@ -16,6 +16,8 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.latin.settings.SettingsValues;
+
import android.content.Context;
import android.media.AudioManager;
import android.os.Vibrator;
@@ -55,10 +57,10 @@ public final class AudioAndHapticFeedbackManager {
mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
}
- public void hapticAndAudioFeedback(final int primaryCode,
+ public void performHapticAndAudioFeedback(final int code,
final View viewToPerformHapticFeedbackOn) {
- vibrateInternal(viewToPerformHapticFeedbackOn);
- playKeyClick(primaryCode);
+ performHapticFeedback(viewToPerformHapticFeedbackOn);
+ performAudioFeedback(code);
}
public boolean hasVibrator() {
@@ -79,14 +81,14 @@ public final class AudioAndHapticFeedbackManager {
return mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;
}
- private void playKeyClick(final int primaryCode) {
+ public void performAudioFeedback(final int code) {
// if mAudioManager is null, we can't play a sound anyway, so return
if (mAudioManager == null) {
return;
}
if (mSoundOn) {
final int sound;
- switch (primaryCode) {
+ switch (code) {
case Constants.CODE_DELETE:
sound = AudioManager.FX_KEYPRESS_DELETE;
break;
@@ -104,7 +106,7 @@ public final class AudioAndHapticFeedbackManager {
}
}
- private void vibrateInternal(final View viewToPerformHapticFeedbackOn) {
+ public void performHapticFeedback(final View viewToPerformHapticFeedbackOn) {
if (!mSettingsValues.mVibrateOn) {
return;
}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index aad129d76..a463651d5 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -19,31 +19,45 @@ package com.android.inputmethod.latin;
import android.text.TextUtils;
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.settings.NativeSuggestOptions;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.JniUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
+import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
+import java.util.Map;
/**
* Implements a static, compacted, binary dictionary of standard words.
*/
+// TODO: All methods which should be locked need to have a suffix "Locked".
public final class BinaryDictionary extends Dictionary {
private static final String TAG = BinaryDictionary.class.getSimpleName();
// Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h
- private static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH;
+ private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
// Must be equal to MAX_RESULTS in native/jni/src/defines.h
private static final int MAX_RESULTS = 18;
+ // Required space count for auto commit.
+ // TODO: Remove this heuristic.
+ private static final int SPACE_COUNT_FOR_AUTO_COMMIT = 3;
private long mNativeDict;
private final Locale mLocale;
+ private final long mDictSize;
+ private final String mDictFilePath;
private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH];
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];
+ private final int[] mOutputAutoCommitFirstWordConfidence = new int[MAX_RESULTS];
private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions();
@@ -58,7 +72,7 @@ public final class BinaryDictionary extends Dictionary {
if (traverseSession == null) {
traverseSession = mDicTraverseSessions.get(traverseSessionId);
if (traverseSession == null) {
- traverseSession = new DicTraverseSession(mLocale, mNativeDict);
+ traverseSession = new DicTraverseSession(mLocale, mNativeDict, mDictSize);
mDicTraverseSessions.put(traverseSessionId, traverseSession);
}
}
@@ -74,49 +88,81 @@ public final class BinaryDictionary extends Dictionary {
* @param length the length of the binary data.
* @param useFullEditDistance whether to use the full edit distance in suggestions
* @param dictType the dictionary type, as a human-readable string
+ * @param isUpdatable whether to open the dictionary file in writable mode.
*/
public BinaryDictionary(final String filename, final long offset, final long length,
- final boolean useFullEditDistance, final Locale locale, final String dictType) {
+ final boolean useFullEditDistance, final Locale locale, final String dictType,
+ final boolean isUpdatable) {
super(dictType);
mLocale = locale;
+ mDictSize = length;
+ mDictFilePath = filename;
mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance);
- loadDictionary(filename, offset, length);
+ loadDictionary(filename, offset, length, isUpdatable);
}
static {
JniUtils.loadNativeLibrary();
}
- private static native long openNative(String sourceDir, long dictOffset, long dictSize);
+ 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 flushNative(long dict, String filePath);
+ private static native boolean needsToRunGCNative(long dict);
+ private static native void flushWithGCNative(long dict, String filePath);
private static native void closeNative(long dict);
private static native int getProbabilityNative(long dict, int[] word);
- private static native boolean isValidBigramNative(long dict, int[] word1, int[] word2);
+ private static native int getBigramProbabilityNative(long dict, int[] word0, int[] word1);
private static native int 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[] 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);
+ private static native void addBigramWordsNative(long dict, int[] word0, int[] word1,
+ int probability);
+ private static native void removeBigramWordsNative(long dict, int[] word0, int[] word1);
+ private static native int calculateProbabilityNative(long dict, int unigramProbability,
+ int bigramProbability);
+
+ @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);
+ }
// TODO: Move native dict into session
private final void loadDictionary(final String path, final long startOffset,
- final long length) {
- mNativeDict = openNative(path, startOffset, length);
+ final long length, final boolean isUpdatable) {
+ mNativeDict = openNative(path, startOffset, length, isUpdatable);
}
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords,
- 0 /* sessionId */);
+ additionalFeaturesOptions, 0 /* sessionId */);
}
@Override
public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords, final int sessionId) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+ final int sessionId) {
if (!isValidDictionary()) return null;
Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE);
@@ -136,15 +182,14 @@ public final class BinaryDictionary extends Dictionary {
final InputPointers ips = composer.getInputPointers();
final int inputSize = isGesture ? ips.getPointerSize() : composerSize;
mNativeSuggestOptions.setIsGesture(isGesture);
- mNativeSuggestOptions.setAdditionalFeaturesOptions(
- AdditionalFeaturesSettingUtils.getAdditionalNativeSuggestOptions());
+ mNativeSuggestOptions.setAdditionalFeaturesOptions(additionalFeaturesOptions);
// 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);
+ mOutputTypes, mOutputAutoCommitFirstWordConfidence);
final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
for (int j = 0; j < count; ++j) {
final int start = j * MAX_WORD_LENGTH;
@@ -167,7 +212,9 @@ public final class BinaryDictionary extends Dictionary {
// TODO: check that all users of the `kind' parameter are ready to accept
// flags too and pass mOutputTypes[j] instead of kind
suggestions.add(new SuggestedWordInfo(new String(mOutputCodePoints, start, len),
- score, kind, mDictType));
+ score, kind, this /* sourceDict */,
+ mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */,
+ mOutputAutoCommitFirstWordConfidence[0]));
}
}
return suggestions;
@@ -193,23 +240,111 @@ public final class BinaryDictionary extends Dictionary {
@Override
public boolean isValidWord(final String word) {
- return getFrequency(word) >= 0;
+ return getFrequency(word) != NOT_A_PROBABILITY;
}
@Override
public int getFrequency(final String word) {
- if (word == null) return -1;
+ if (word == null) return NOT_A_PROBABILITY;
int[] codePoints = StringUtils.toCodePointArray(word);
return getProbabilityNative(mNativeDict, codePoints);
}
// TODO: Add a batch process version (isValidBigramMultiple?) to avoid excessive numbers of jni
// calls when checking for changes in an entire dictionary.
- public boolean isValidBigram(final String word1, final String word2) {
- if (TextUtils.isEmpty(word1) || TextUtils.isEmpty(word2)) return false;
+ public boolean isValidBigram(final String word0, final String word1) {
+ return getBigramProbability(word0, word1) != NOT_A_PROBABILITY;
+ }
+
+ public int getBigramProbability(final String word0, final String word1) {
+ if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) return NOT_A_PROBABILITY;
+ final int[] codePoints0 = StringUtils.toCodePointArray(word0);
+ final int[] codePoints1 = StringUtils.toCodePointArray(word1);
+ return getBigramProbabilityNative(mNativeDict, codePoints0, codePoints1);
+ }
+
+ private void runGCIfRequired() {
+ if (needsToRunGCNative(mNativeDict)) {
+ flushWithGC();
+ }
+ }
+
+ // Add a unigram entry to binary dictionary in native code.
+ public void addUnigramWord(final String word, final int probability) {
+ if (TextUtils.isEmpty(word)) {
+ return;
+ }
+ runGCIfRequired();
+ final int[] codePoints = StringUtils.toCodePointArray(word);
+ addUnigramWordNative(mNativeDict, codePoints, probability);
+ }
+
+ // Add a bigram entry to binary dictionary in native code.
+ public void addBigramWords(final String word0, final String word1, final int probability) {
+ if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) {
+ return;
+ }
+ runGCIfRequired();
+ final int[] codePoints0 = StringUtils.toCodePointArray(word0);
+ final int[] codePoints1 = StringUtils.toCodePointArray(word1);
+ addBigramWordsNative(mNativeDict, codePoints0, codePoints1, probability);
+ }
+
+ // Remove a bigram entry form binary dictionary in native code.
+ public void removeBigramWords(final String word0, final String word1) {
+ if (TextUtils.isEmpty(word0) || TextUtils.isEmpty(word1)) {
+ return;
+ }
+ runGCIfRequired();
+ final int[] codePoints0 = StringUtils.toCodePointArray(word0);
final int[] codePoints1 = StringUtils.toCodePointArray(word1);
- final int[] codePoints2 = StringUtils.toCodePointArray(word2);
- return isValidBigramNative(mNativeDict, codePoints1, codePoints2);
+ removeBigramWordsNative(mNativeDict, codePoints0, codePoints1);
+ }
+
+ private void reopen() {
+ close();
+ final File dictFile = new File(mDictFilePath);
+ mNativeDict = openNative(dictFile.getAbsolutePath(), 0 /* startOffset */,
+ dictFile.length(), true /* isUpdatable */);
+ }
+
+ public void flush() {
+ if (!isValidDictionary()) return;
+ flushNative(mNativeDict, mDictFilePath);
+ reopen();
+ }
+
+ public void flushWithGC() {
+ if (!isValidDictionary()) return;
+ flushWithGCNative(mNativeDict, mDictFilePath);
+ reopen();
+ }
+
+ public boolean needsToRunGC() {
+ if (!isValidDictionary()) return false;
+ return needsToRunGCNative(mNativeDict);
+ }
+
+ @UsedForTesting
+ public int calculateProbability(final int unigramProbability, final int bigramProbability) {
+ if (!isValidDictionary()) return NOT_A_PROBABILITY;
+ return calculateProbabilityNative(mNativeDict, unigramProbability, bigramProbability);
+ }
+
+ @Override
+ public boolean shouldAutoCommit(final SuggestedWordInfo candidate) {
+ // TODO: actually use the confidence rather than use this completely broken heuristic
+ final String word = candidate.mWord;
+ final int length = word.length();
+ int remainingSpaces = SPACE_COUNT_FOR_AUTO_COMMIT;
+ for (int i = 0; i < length; ++i) {
+ // This is okay because no low-surrogate and no high-surrogate can ever match the
+ // space character, so we don't need to take care of iterating on code points.
+ if (Constants.CODE_SPACE == word.charAt(i)) {
+ if (0 >= --remainingSpaces) return true;
+ }
+ }
+ return false;
}
@Override
@@ -222,21 +357,23 @@ public final class BinaryDictionary extends Dictionary {
traverseSession.close();
}
}
+ mDicTraverseSessions.clear();
}
- closeInternal();
+ closeInternalLocked();
}
- private synchronized void closeInternal() {
+ private synchronized void closeInternalLocked() {
if (mNativeDict != 0) {
closeNative(mNativeDict);
mNativeDict = 0;
}
}
+ // TODO: Manage BinaryDictionary instances without using WeakReference or something.
@Override
protected void finalize() throws Throwable {
try {
- closeInternal();
+ closeInternalLocked();
} finally {
super.finalize();
}
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index c038db87c..722a82961 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -28,7 +28,11 @@ import android.text.TextUtils;
import android.util.Log;
import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
-import com.android.inputmethod.latin.DictionaryInfoUtils.DictionaryInfo;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
+import com.android.inputmethod.latin.utils.DictionaryInfoUtils.DictionaryInfo;
+import com.android.inputmethod.latin.utils.FileTransforms;
+import com.android.inputmethod.latin.utils.MetadataFileUriGetter;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -305,6 +309,7 @@ public final class BinaryDictionaryFileDumper {
Log.e(TAG, "Could not have the dictionary pack delete a word list");
}
BinaryDictionaryGetter.removeFilesWithIdExcept(context, wordlistId, finalFile);
+ Log.e(TAG, "Successfully copied file for wordlist ID " + wordlistId);
// Success! Close files (through the finally{} clause) and return.
return;
} catch (Exception e) {
@@ -436,7 +441,6 @@ public final class BinaryDictionaryFileDumper {
final ContentProviderClient client, final String clientId) throws RemoteException {
final String metadataFileUri = MetadataFileUriGetter.getMetadataUri(context);
final String metadataAdditionalId = MetadataFileUriGetter.getMetadataAdditionalId(context);
- if (TextUtils.isEmpty(metadataFileUri)) return;
// Tell the content provider to reset all information about this client id
final Uri metadataContentUri = getProviderUriBuilder(clientId)
.appendPath(QUERY_PATH_METADATA)
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 98eadcacb..181ad17ea 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -16,22 +16,22 @@
package com.android.inputmethod.latin;
-import com.android.inputmethod.latin.define.ProductionFlag;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
-import com.android.inputmethod.latin.makedict.FormatSpec;
-
import android.content.Context;
import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
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.UnsupportedFormatException;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
+import com.android.inputmethod.latin.utils.LocaleUtils;
+
import java.io.File;
-import java.io.FileInputStream;
import java.io.IOException;
import java.nio.BufferUnderflowException;
-import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
@@ -39,7 +39,7 @@ import java.util.Locale;
/**
* Helper class to get the address of a mmap'able dictionary file.
*/
-final class BinaryDictionaryGetter {
+final public class BinaryDictionaryGetter {
/**
* Used for Log actions from this class
@@ -223,31 +223,16 @@ final class BinaryDictionaryGetter {
}
}
- // ## HACK ## we prevent usage of a dictionary before version 18 for English only. 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.
+ // ## 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) {
- // Only for English - other languages didn't have a whitelist, hence this
- // ad-hoc ## HACK ##
- if (!Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) return true;
-
- FileInputStream inStream = null;
try {
// Read the version of the file
- inStream = new FileInputStream(f);
- final BinaryDictInputOutput.ByteBufferWrapper buffer =
- new BinaryDictInputOutput.ByteBufferWrapper(inStream.getChannel().map(
- FileChannel.MapMode.READ_ONLY, 0, f.length()));
- final int magic = buffer.readInt();
- if (magic != FormatSpec.VERSION_2_MAGIC_NUMBER) {
- return false;
- }
- final int formatVersion = buffer.readInt();
- final int headerSize = buffer.readInt();
- final HashMap<String, String> options = CollectionUtils.newHashMap();
- BinaryDictInputOutput.populateOptions(buffer, headerSize, options);
+ final DictDecoder dictDecoder = FormatSpec.getDictDecoder(f);
+ final FileHeader header = dictDecoder.readHeader();
- final String version = options.get(VERSION_KEY);
+ final String version = header.mDictionaryOptions.mAttributes.get(VERSION_KEY);
if (null == version) {
// No version in the options : the format is unexpected
return false;
@@ -263,14 +248,8 @@ final class BinaryDictionaryGetter {
return false;
} catch (BufferUnderflowException e) {
return false;
- } finally {
- if (inStream != null) {
- try {
- inStream.close();
- } catch (IOException e) {
- // do nothing
- }
- }
+ } catch (UnsupportedFormatException e) {
+ return false;
}
}
@@ -290,17 +269,8 @@ final class BinaryDictionaryGetter {
final Context context) {
final boolean hasDefaultWordList = DictionaryFactory.isDictionaryAvailable(context, locale);
- // TODO: The development-only-diagnostic version is not supported by the Dictionary Pack
- // Service yet
- if (!ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- // We need internet access to do the following. Only do this if the package actually
- // has the permission.
- if (context.checkCallingOrSelfPermission(android.Manifest.permission.INTERNET)
- == PackageManager.PERMISSION_GRANTED) {
- BinaryDictionaryFileDumper.cacheWordListsFromContentProvider(locale, context,
- hasDefaultWordList);
- }
- }
+ BinaryDictionaryFileDumper.cacheWordListsFromContentProvider(locale, context,
+ hasDefaultWordList);
final File[] cachedWordLists = getCachedWordLists(locale.toString(), context);
final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);
final DictPackSettings dictPackSettings = new DictPackSettings(context);
@@ -316,7 +286,8 @@ final class BinaryDictionaryGetter {
}
if (!dictPackSettings.isWordListActive(wordListId)) continue;
if (canUse) {
- fileList.add(AssetFileAddress.makeFromFileName(f.getPath()));
+ final AssetFileAddress afa = AssetFileAddress.makeFromFileName(f.getPath());
+ if (null != afa) fileList.add(afa);
} else {
Log.e(TAG, "Found a cached dictionary file but cannot read or use it");
}
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index bb4a42ede..c4f96016c 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -76,6 +76,11 @@ public final class Constants {
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.
+ */
+ public static final String EMOJI_CAPABLE = "EmojiCapable";
+ /**
* The subtype extra value used to indicate that the subtype require network connection
* to work.
*/
@@ -126,15 +131,6 @@ public final class Constants {
}
}
- public static final class Dictionary {
- // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h
- public static final int MAX_WORD_LENGTH = 48;
-
- private Dictionary() {
- // This utility class is no publicly instantiable.
- }
- }
-
public static final int NOT_A_CODE = -1;
public static final int NOT_A_COORDINATE = -1;
@@ -142,6 +138,13 @@ public final class Constants {
public static final int SPELL_CHECKER_COORDINATE = -3;
public static final int EXTERNAL_KEYBOARD_COORDINATE = -4;
+ // A hint on how many characters to cache from the TextView. A good value of this is given by
+ // how many characters we need to be able to almost always find the caps mode.
+ public static final int EDITOR_CONTENTS_CACHE_SIZE = 1024;
+
+ // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h
+ public static final int DICTIONARY_MAX_WORD_LENGTH = 48;
+
public static boolean isValidCoordinate(final int coordinate) {
// Detect {@link NOT_A_COORDINATE}, {@link SUGGESTION_STRIP_COORDINATE},
// and {@link SPELL_CHECKER_COORDINATE}.
@@ -149,12 +152,20 @@ public final class Constants {
}
/**
+ * Custom request code used in
+ * {@link com.android.inputmethod.keyboard.KeyboardActionListener#onCustomRequest(int)}.
+ */
+ // The code to show input method picker.
+ public static final int CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER = 1;
+
+ /**
* Some common keys code. Must be positive.
*/
public static final int CODE_ENTER = '\n';
public static final int CODE_TAB = '\t';
public static final int CODE_SPACE = ' ';
public static final int CODE_PERIOD = '.';
+ public static final int CODE_ARMENIAN_PERIOD = 0x0589;
public static final int CODE_DASH = '-';
public static final int CODE_SINGLE_QUOTE = '\'';
public static final int CODE_DOUBLE_QUOTE = '"';
@@ -162,9 +173,7 @@ public final class Constants {
public static final int CODE_EXCLAMATION_MARK = '!';
public static final int CODE_SLASH = '/';
public static final int CODE_COMMERCIAL_AT = '@';
- // TODO: Check how this should work for right-to-left languages. It seems to stand
- // that for rtl languages, a closing parenthesis is a left parenthesis. Is this
- // managed by the font? Or is it a different char?
+ public static final int CODE_PLUS = '+';
public static final int CODE_CLOSING_PARENTHESIS = ')';
public static final int CODE_CLOSING_SQUARE_BRACKET = ']';
public static final int CODE_CLOSING_CURLY_BRACKET = '}';
@@ -185,7 +194,7 @@ public final class Constants {
public static final int CODE_ACTION_NEXT = -8;
public static final int CODE_ACTION_PREVIOUS = -9;
public static final int CODE_LANGUAGE_SWITCH = -10;
- public static final int CODE_RESEARCH = -11;
+ public static final int CODE_EMOJI = -11;
public static final int CODE_SHIFT_ENTER = -12;
// Code value representing the code is not specified.
public static final int CODE_UNSPECIFIED = -13;
@@ -206,10 +215,11 @@ public final class Constants {
case CODE_ACTION_NEXT: return "actionNext";
case CODE_ACTION_PREVIOUS: return "actionPrevious";
case CODE_LANGUAGE_SWITCH: return "languageSwitch";
+ case CODE_EMOJI: return "emoji";
+ case CODE_SHIFT_ENTER: return "shiftEnter";
case CODE_UNSPECIFIED: return "unspec";
case CODE_TAB: return "tab";
case CODE_ENTER: return "enter";
- case CODE_RESEARCH: return "research";
default:
if (code < CODE_SPACE) return String.format("'\\u%02x'", code);
if (code < 0x100) return String.format("'%c'", code);
@@ -217,7 +227,10 @@ public final class Constants {
}
}
+ public static final int MAX_INT_BIT_COUNT = 32;
+
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 b9db9a092..ffeb92784 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -22,6 +22,7 @@ import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.os.SystemClock;
import android.provider.BaseColumns;
@@ -30,6 +31,8 @@ import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.util.Log;
+import com.android.inputmethod.latin.utils.StringUtils;
+
import java.util.List;
import java.util.Locale;
@@ -68,7 +71,8 @@ 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);
+ super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS,
+ false /* isUpdatable */);
mLocale = locale;
mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
registerObserver(context);
@@ -107,7 +111,6 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
@Override
public void loadDictionaryAsync() {
- clearFusionDictionary();
loadDeviceAccountsEmailAddresses();
loadDictionaryAsyncForUri(ContactsContract.Profile.CONTENT_URI);
// TODO: Switch this URL to the newer ContactsContract too
@@ -143,8 +146,10 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
cursor.close();
}
}
- } catch (IllegalStateException e) {
- Log.e(TAG, "Contacts DB is having problems");
+ } 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);
}
}
@@ -171,14 +176,18 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
private int getContactCount() {
// TODO: consider switching to a rawQuery("select count(*)...") on the database if
// performance is a bottleneck.
- 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();
+ 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();
+ }
}
+ } catch (final SQLiteException e) {
+ Log.e(TAG, "SQLiteException in the remote Contacts process.", e);
}
return 0;
}
@@ -207,7 +216,8 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
false /* isNotAWord */);
if (!TextUtils.isEmpty(prevWord)) {
if (mUseFirstLastBigrams) {
- super.setBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM);
+ super.addBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM,
+ 0 /* lastModifiedTime */);
}
}
prevWord = word;
@@ -234,6 +244,11 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
}
@Override
+ protected boolean needsToReloadBeforeWriting() {
+ return true;
+ }
+
+ @Override
protected boolean hasContentChanged() {
final long startTime = SystemClock.uptimeMillis();
final int contactCount = getContactCount();
diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
index 534e2116b..8d295adee 100644
--- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java
+++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
@@ -16,6 +16,8 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.latin.utils.JniUtils;
+
import java.util.Locale;
public final class DicTraverseSession {
@@ -23,16 +25,16 @@ public final class DicTraverseSession {
JniUtils.loadNativeLibrary();
}
- private static native long setDicTraverseSessionNative(String locale);
+ private static native long setDicTraverseSessionNative(String locale, long dictSize);
private static native void initDicTraverseSessionNative(long nativeDicTraverseSession,
long dictionary, int[] previousWord, int previousWordLength);
private static native void releaseDicTraverseSessionNative(long nativeDicTraverseSession);
private long mNativeDicTraverseSession;
- public DicTraverseSession(Locale locale, long dictionary) {
+ public DicTraverseSession(Locale locale, long dictionary, long dictSize) {
mNativeDicTraverseSession = createNativeDicTraverseSession(
- locale != null ? locale.toString() : "");
+ locale != null ? locale.toString() : "", dictSize);
initSession(dictionary);
}
@@ -49,8 +51,8 @@ public final class DicTraverseSession {
mNativeDicTraverseSession, dictionary, previousWord, previousWordLength);
}
- private final long createNativeDicTraverseSession(String locale) {
- return setDicTraverseSessionNative(locale);
+ private final long createNativeDicTraverseSession(String locale, long dictSize) {
+ return setDicTraverseSessionNative(locale, dictSize);
}
private void closeInternal() {
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index acd7c2aa1..fa79f5af7 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -28,18 +28,38 @@ import java.util.ArrayList;
public abstract class Dictionary {
public static final int NOT_A_PROBABILITY = -1;
+ // The following types do not actually come from real dictionary instances, so we create
+ // corresponding instances.
public static final String TYPE_USER_TYPED = "user_typed";
+ public static final Dictionary DICTIONARY_USER_TYPED = new PhonyDictionary(TYPE_USER_TYPED);
+
public static final String TYPE_APPLICATION_DEFINED = "application_defined";
+ public static final Dictionary DICTIONARY_APPLICATION_DEFINED =
+ new PhonyDictionary(TYPE_APPLICATION_DEFINED);
+
public static final String TYPE_HARDCODED = "hardcoded"; // punctuation signs and such
+ public static final Dictionary DICTIONARY_HARDCODED =
+ new PhonyDictionary(TYPE_HARDCODED);
+
+ // Spawned by resuming suggestions. Comes from a span that was in the TextView.
+ public static final String TYPE_RESUMED = "resumed";
+ public static final Dictionary DICTIONARY_RESUMED =
+ new PhonyDictionary(TYPE_RESUMED);
+
+ // The following types of dictionary have actual functional instances. We don't need final
+ // phony dictionary instances for them.
public static final String TYPE_MAIN = "main";
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.
+ // User history dictionary internal to LatinIME. This assumes bigram prediction for now.
public static final String TYPE_USER_HISTORY = "history";
- // Spawned by resuming suggestions. Comes from a span that was in the TextView.
- public static final String TYPE_RESUMED = "resumed";
- protected final String mDictType;
+ // Personalization binary dictionary internal to LatinIME.
+ 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) {
mDictType = dictType;
@@ -52,20 +72,23 @@ public abstract class Dictionary {
* @param prevWord the previous word, or null if none
* @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.
* @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 boolean blockOffensiveWords, final int[] additionalFeaturesOptions);
// 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 sessionId) {
- return getSuggestions(composer, prevWord, proximityInfo, blockOffensiveWords);
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+ final int sessionId) {
+ return getSuggestions(composer, prevWord, proximityInfo, blockOffensiveWords,
+ additionalFeaturesOptions);
}
/**
@@ -109,8 +132,43 @@ public abstract class Dictionary {
/**
* Subclasses may override to indicate that this Dictionary is not yet properly initialized.
*/
-
public boolean isInitialized() {
return true;
}
+
+ /**
+ * Whether we think this suggestion should trigger an auto-commit. prevWord is the word
+ * before the suggestion, so that we can use n-gram frequencies.
+ * @param candidate The candidate suggestion, in whole (not only the first part).
+ * @return whether we should auto-commit or not.
+ */
+ public boolean shouldAutoCommit(final SuggestedWordInfo candidate) {
+ // If we don't have support for auto-commit, or if we don't know, we return false to
+ // avoid auto-committing stuff. Implementations of the Dictionary class that know to
+ // determine whether we should auto-commit will override this.
+ return false;
+ }
+
+ /**
+ * Not a true dictionary. A placeholder used to indicate suggestions that don't come from any
+ * real dictionary.
+ */
+ private static class PhonyDictionary extends Dictionary {
+ // This class is not publicly instantiable.
+ private PhonyDictionary(final String type) {
+ super(type);
+ }
+
+ @Override
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
+ return null;
+ }
+
+ @Override
+ public boolean isValidWord(String word) {
+ return false;
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index ed2b44223..bf075140e 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -16,10 +16,11 @@
package com.android.inputmethod.latin;
+import android.util.Log;
+
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-
-import android.util.Log;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.ArrayList;
import java.util.Collection;
@@ -57,18 +58,18 @@ public final class DictionaryCollection extends Dictionary {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
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);
+ prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions);
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);
+ prevWord, proximityInfo, blockOffensiveWords, additionalFeaturesOptions);
if (null != sugg) suggestions.addAll(sugg);
}
return suggestions;
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index 4514ec2ec..3721132c5 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -22,6 +22,8 @@ import android.content.res.Resources;
import android.util.Log;
import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
import java.io.File;
import java.util.ArrayList;
@@ -58,7 +60,8 @@ public final class DictionaryFactory {
if (null != assetFileList) {
for (final AssetFileAddress f : assetFileList) {
final BinaryDictionary binaryDictionary = new BinaryDictionary(f.mFilename,
- f.mOffset, f.mLength, useFullEditDistance, locale, Dictionary.TYPE_MAIN);
+ f.mOffset, f.mLength, useFullEditDistance, locale, Dictionary.TYPE_MAIN,
+ false /* isUpdatable */);
if (binaryDictionary.isValidDictionary()) {
dictList.add(binaryDictionary);
}
@@ -111,7 +114,8 @@ public final class DictionaryFactory {
return null;
}
return new BinaryDictionary(sourceDir, afd.getStartOffset(), afd.getLength(),
- false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
+ false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN,
+ false /* isUpdatable */);
} catch (android.content.res.Resources.NotFoundException e) {
Log.e(TAG, "Could not find the resource");
return null;
@@ -140,7 +144,7 @@ public final class DictionaryFactory {
for (final AssetFileAddress address : dictionaryList) {
final BinaryDictionary binaryDictionary = new BinaryDictionary(address.mFilename,
address.mOffset, address.mLength, useFullEditDistance, locale,
- Dictionary.TYPE_MAIN);
+ Dictionary.TYPE_MAIN, false /* isUpdatable */);
dictionaryCollection.addDictionary(binaryDictionary);
}
return dictionaryCollection;
diff --git a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
index 56096127e..2dcfdb0b7 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
@@ -17,6 +17,7 @@
package com.android.inputmethod.latin;
import com.android.inputmethod.dictionarypack.DictionaryPackConstants;
+import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
import android.content.BroadcastReceiver;
import android.content.Context;
diff --git a/java/src/com/android/inputmethod/latin/DictionaryWriter.java b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
new file mode 100644
index 000000000..5a453dde5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryWriter.java
@@ -0,0 +1,105 @@
+/*
+ * 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;
+
+/**
+ * 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 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, frequency));
+ 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)
+ throws IOException, UnsupportedFormatException {
+ 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 887d657e9..99859decf 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -20,21 +20,21 @@ 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.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput;
import com.android.inputmethod.latin.makedict.FormatSpec;
-import com.android.inputmethod.latin.makedict.FusionDictionary;
-import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
-import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+import com.android.inputmethod.latin.personalization.DynamicPersonalizationDictionaryWriter;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.utils.AsyncResultHolder;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.PrioritizedSerialExecutor;
import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
/**
* Abstract base class for an expandable dictionary that can be created and updated dynamically
@@ -52,19 +52,32 @@ 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 int TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS = 100;
+
/**
* The maximum length of a word in this dictionary.
*/
- protected static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH;
+ 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;
/**
- * A static map of locks, each of which controls access to a single binary dictionary file. They
- * ensure that only one instance can update the same dictionary at the same time. The key for
- * this map is the filename and the value is the shared dictionary controller associated with
- * that filename.
+ * A static map of time recorders, each of which records the time of accesses to a single binary
+ * dictionary file. The key for this map is the filename and the value is the shared dictionary
+ * time recorder associated with that filename.
*/
- private static final HashMap<String, DictionaryController> sSharedDictionaryControllers =
- CollectionUtils.newHashMap();
+ private static volatile ConcurrentHashMap<String, DictionaryTimeRecorder>
+ sFilenameDictionaryTimeRecorderMap = CollectionUtils.newConcurrentHashMap();
+
+ private static volatile ConcurrentHashMap<String, PrioritizedSerialExecutor>
+ sFilenameExecutorMap = CollectionUtils.newConcurrentHashMap();
/** The application context. */
protected final Context mContext;
@@ -75,25 +88,34 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
private BinaryDictionary mBinaryDictionary;
- /** The expandable fusion dictionary used to generate the binary dictionary. */
- private FusionDictionary mFusionDictionary;
+ // 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
- * DictionaryController.
+ * DictionaryTimeRecorder.
*/
private final String mFilename;
- /** Controls access to the shared binary dictionary file across multiple instances. */
- private final DictionaryController mSharedDictionaryController;
+ /** Whether to support dynamically updating the dictionary */
+ private final boolean mIsUpdatable;
+
+ // TODO: remove, once dynamic operations is serialized
+ /** Records access to the shared binary dictionary file across multiple instances. */
+ private final DictionaryTimeRecorder mFilenameDictionaryTimeRecorder;
+
+ // TODO: remove, once dynamic operations is serialized
+ /** Records access to the local binary dictionary for this instance. */
+ private final DictionaryTimeRecorder mPerInstanceDictionaryTimeRecorder =
+ new DictionaryTimeRecorder();
- /** Controls access to the local binary dictionary for this instance. */
- private final DictionaryController mLocalDictionaryController = new DictionaryController();
+ /* A extension for a binary dictionary file. */
+ public static final String DICT_FILE_EXTENSION = ".dict";
- private static final int BINARY_DICT_VERSION = 1;
- private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
- new FormatSpec.FormatOptions(BINARY_DICT_VERSION);
+ private final AtomicReference<Runnable> mUnfinishedFlushingTask =
+ new AtomicReference<Runnable>();
/**
* Abstract method for loading the unigrams and bigrams of a given dictionary in a background
@@ -109,16 +131,45 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
protected abstract boolean hasContentChanged();
/**
- * Gets the shared dictionary controller for the given filename.
+ * Gets the dictionary time recorder for the given filename.
*/
- private static synchronized DictionaryController getSharedDictionaryController(
+ private static DictionaryTimeRecorder getDictionaryTimeRecorder(
String filename) {
- DictionaryController controller = sSharedDictionaryControllers.get(filename);
- if (controller == null) {
- controller = new DictionaryController();
- sSharedDictionaryControllers.put(filename, controller);
+ DictionaryTimeRecorder recorder = sFilenameDictionaryTimeRecorderMap.get(filename);
+ if (recorder == null) {
+ synchronized(sFilenameDictionaryTimeRecorderMap) {
+ recorder = new DictionaryTimeRecorder();
+ sFilenameDictionaryTimeRecorderMap.put(filename, 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);
}
- return controller;
}
/**
@@ -128,19 +179,23 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* @param filename The filename for this binary dictionary. Multiple dictionaries with the same
* filename is supported.
* @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.
*/
- public ExpandableBinaryDictionary(
- final Context context, final String filename, final String dictType) {
+ public ExpandableBinaryDictionary(final Context context, final String filename,
+ final String dictType, final boolean isUpdatable) {
super(dictType);
mFilename = filename;
mContext = context;
+ mIsUpdatable = isUpdatable;
mBinaryDictionary = null;
- mSharedDictionaryController = getSharedDictionaryController(filename);
- clearFusionDictionary();
+ mFilenameDictionaryTimeRecorder = getDictionaryTimeRecorder(filename);
+ // Currently, only dynamic personalization dictionary is updatable.
+ mDictionaryWriter = getDictionaryWriter(context, dictType, isUpdatable);
}
protected static String getFilenameWithLocale(final String name, final String localeStr) {
- return name + "." + localeStr + ".dict";
+ return name + "." + localeStr + DICT_FILE_EXTENSION;
}
/**
@@ -148,89 +203,218 @@ 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.
- mLocalDictionaryController.lock();
- try {
- if (mBinaryDictionary != null) {
- mBinaryDictionary.close();
- mBinaryDictionary = null;
+ getExecutor(mFilename).execute(new Runnable() {
+ @Override
+ public void run() {
+ if (mBinaryDictionary != null) {
+ mBinaryDictionary.close();
+ mBinaryDictionary = null;
+ }
}
- } finally {
- mLocalDictionaryController.unlock();
- }
+ });
+ }
+
+ protected Map<String, String> getHeaderAttributeMap() {
+ HashMap<String, String> attributeMap = new HashMap<String, String>();
+ attributeMap.put(FormatSpec.FileHeader.SUPPORTS_DYNAMIC_UPDATE_ATTRIBUTE,
+ SUPPORTS_DYNAMIC_UPDATE);
+ return attributeMap;
+ }
+
+ protected void clear() {
+ getExecutor(mFilename).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());
+ } else {
+ mDictionaryWriter.clear();
+ }
+ }
+ });
}
/**
- * Clears the fusion dictionary on the Java side. Note: Does not modify the binary dictionary on
- * the native side.
+ * Adds a word unigram to the dictionary. Used for loading a dictionary.
*/
- public void clearFusionDictionary() {
- final HashMap<String, String> attributes = CollectionUtils.newHashMap();
- mFusionDictionary = new FusionDictionary(new Node(),
- new FusionDictionary.DictionaryOptions(attributes, false, false));
+ protected void addWord(final String word, final String shortcutTarget,
+ final int frequency, final boolean isNotAWord) {
+ mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
}
/**
- * Adds a word unigram to the fusion dictionary. Call updateBinaryDictionary when all changes
- * are done to update the binary dictionary.
+ * Adds a word bigram in the dictionary. Used for loading a dictionary.
*/
- // TODO: Create "cache dictionary" to cache fresh words for frequently updated dictionaries,
- // considering performance regression.
- protected void addWord(final String word, final String shortcutTarget, final int frequency,
- 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, frequency));
- mFusionDictionary.add(word, frequency, shortcutTargets, isNotAWord);
+ protected void addBigram(final String prevWord, final String word, final int frequency,
+ final long lastModifiedTime) {
+ mDictionaryWriter.addBigramWords(prevWord, word, frequency, true /* isValid */,
+ lastModifiedTime);
+ }
+
+ /**
+ * 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 boolean isNotAWord) {
+ if (!mIsUpdatable) {
+ Log.w(TAG, "addWordDynamically is called for non-updatable dictionary: " + mFilename);
+ return;
}
+
+ getExecutor(mFilename).execute(new Runnable() {
+ @Override
+ public void run() {
+ if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
+ mBinaryDictionary.addUnigramWord(word, frequency);
+ } else {
+ // TODO: Remove.
+ mDictionaryWriter.addUnigramWord(word, shortcutTarget, frequency, isNotAWord);
+ }
+ }
+ });
}
/**
- * Sets a word bigram in the fusion dictionary. Call updateBinaryDictionary when all changes are
- * done to update the binary dictionary.
+ * Dynamically adds a word bigram in the dictionary. May overwrite an existing entry.
*/
- // TODO: Create "cache dictionary" to cache fresh bigrams for frequently updated dictionaries,
- // considering performance regression.
- protected void setBigram(final String prevWord, final String word, final int frequency) {
- mFusionDictionary.setBigram(prevWord, word, frequency);
+ 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() {
+ @Override
+ public void run() {
+ if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
+ mBinaryDictionary.addBigramWords(word0, word1, frequency);
+ } else {
+ // TODO: Remove.
+ mDictionaryWriter.addBigramWords(word0, word1, frequency, isValid,
+ 0 /* lastTouchedTime */);
+ }
+ }
+ });
+ }
+
+ /**
+ * 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() {
+ @Override
+ public void run() {
+ if (ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
+ mBinaryDictionary.removeBigramWords(word0, word1);
+ } else {
+ // TODO: Remove.
+ mDictionaryWriter.removeBigramWords(word0, word1);
+ }
+ }
+ });
}
@Override
- public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
- asyncReloadDictionaryIfRequired();
- if (mLocalDictionaryController.tryLock()) {
- try {
- if (mBinaryDictionary != null) {
- return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo,
- blockOffensiveWords);
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+ final int sessionId) {
+ reloadDictionaryIfRequired();
+ final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
+ final AsyncResultHolder<ArrayList<SuggestedWordInfo>> holder =
+ new AsyncResultHolder<ArrayList<SuggestedWordInfo>>();
+ getExecutor(mFilename).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);
+ }
}
- } finally {
- mLocalDictionaryController.unlock();
}
- }
- return null;
+ });
+ return holder.get(null, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
+ }
+
+ @Override
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final String prevWord, final ProximityInfo proximityInfo,
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
+ return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, blockOffensiveWords,
+ additionalFeaturesOptions, 0 /* sessionId */);
}
@Override
public boolean isValidWord(final String word) {
- asyncReloadDictionaryIfRequired();
+ reloadDictionaryIfRequired();
return isValidWordInner(word);
}
protected boolean isValidWordInner(final String word) {
- if (mLocalDictionaryController.tryLock()) {
- try {
- return isValidWordLocked(word);
- } finally {
- mLocalDictionaryController.unlock();
+ final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>();
+ getExecutor(mFilename).executePrioritized(new Runnable() {
+ @Override
+ public void run() {
+ holder.set(isValidWordLocked(word));
}
- }
- return false;
+ });
+ return holder.get(false, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
}
protected boolean isValidWordLocked(final String word) {
@@ -238,22 +422,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
return mBinaryDictionary.isValidWord(word);
}
- protected boolean isValidBigram(final String word1, final String word2) {
- if (mBinaryDictionary == null) return false;
- return mBinaryDictionary.isValidBigram(word1, word2);
- }
-
- protected boolean isValidBigramInner(final String word1, final String word2) {
- if (mLocalDictionaryController.tryLock()) {
- try {
- return isValidBigramLocked(word1, word2);
- } finally {
- mLocalDictionaryController.unlock();
- }
- }
- return false;
- }
-
protected boolean isValidBigramLocked(final String word1, final String word2) {
if (mBinaryDictionary == null) return false;
return mBinaryDictionary.isValidBigram(word1, word2);
@@ -264,19 +432,19 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* dictionary exists, this method will generate one.
*/
protected void loadDictionary() {
- mLocalDictionaryController.mLastUpdateRequestTime = SystemClock.uptimeMillis();
- asyncReloadDictionaryIfRequired();
+ mPerInstanceDictionaryTimeRecorder.mLastUpdateRequestTime = SystemClock.uptimeMillis();
+ reloadDictionaryIfRequired();
}
/**
* Loads the current binary dictionary from internal storage. Assumes the dictionary file
* exists.
*/
- protected void loadBinaryDictionary() {
+ private void loadBinaryDictionary() {
if (DEBUG) {
Log.d(TAG, "Loading binary dictionary: " + mFilename + " request="
- + mSharedDictionaryController.mLastUpdateRequestTime + " update="
- + mSharedDictionaryController.mLastUpdateTime);
+ + mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime + " update="
+ + mFilenameDictionaryTimeRecorder.mLastUpdateTime);
}
final File file = new File(mContext.getFilesDir(), mFilename);
@@ -285,55 +453,57 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
// Build the new binary dictionary
final BinaryDictionary newBinaryDictionary = new BinaryDictionary(filename, 0, length,
- true /* useFullEditDistance */, null, mDictType);
-
- if (mBinaryDictionary != null) {
- // Ensure all threads accessing the current dictionary have finished before swapping in
- // the new one.
- final BinaryDictionary oldBinaryDictionary = mBinaryDictionary;
- mLocalDictionaryController.lock();
- mBinaryDictionary = newBinaryDictionary;
- mLocalDictionaryController.unlock();
- oldBinaryDictionary.close();
- } else {
- mBinaryDictionary = newBinaryDictionary;
- }
+ 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();
+ }
+ }
+ });
}
/**
- * Generates and writes a new binary dictionary based on the contents of the fusion dictionary.
+ * Abstract method for checking if it is required to reload the dictionary before writing
+ * a binary dictionary.
*/
- private void generateBinaryDictionary() {
+ abstract protected boolean needsToReloadBeforeWriting();
+
+ /**
+ * Writes a new binary dictionary based on the contents of the fusion dictionary.
+ */
+ private void writeBinaryDictionary() {
if (DEBUG) {
Log.d(TAG, "Generating binary dictionary: " + mFilename + " request="
- + mSharedDictionaryController.mLastUpdateRequestTime + " update="
- + mSharedDictionaryController.mLastUpdateTime);
+ + mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime + " update="
+ + mFilenameDictionaryTimeRecorder.mLastUpdateTime);
}
-
- loadDictionaryAsync();
-
- final String tempFileName = mFilename + ".temp";
- final File file = new File(mContext.getFilesDir(), mFilename);
- final File tempFile = new File(mContext.getFilesDir(), tempFileName);
- FileOutputStream out = null;
- try {
- out = new FileOutputStream(tempFile);
- BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, FORMAT_OPTIONS);
- out.flush();
- out.close();
- tempFile.renameTo(file);
- clearFusionDictionary();
- } catch (IOException e) {
- Log.e(TAG, "IO exception while writing file", e);
- } catch (UnsupportedFormatException e) {
- Log.e(TAG, "Unsupported format", e);
- } finally {
- if (out != null) {
- try {
- out.close();
- } catch (IOException e) {
- // ignore
+ if (needsToReloadBeforeWriting()) {
+ mDictionaryWriter.clear();
+ loadDictionaryAsync();
+ mDictionaryWriter.write(mFilename);
+ } 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()) {
+ mBinaryDictionary.flushWithGC();
+ } else {
+ mBinaryDictionary.flush();
+ }
}
+ } else {
+ mDictionaryWriter.write(mFilename);
}
}
}
@@ -347,77 +517,74 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
*/
protected void setRequiresReload(final boolean requiresRebuild) {
final long time = SystemClock.uptimeMillis();
- mLocalDictionaryController.mLastUpdateRequestTime = time;
- mSharedDictionaryController.mLastUpdateRequestTime = time;
+ mPerInstanceDictionaryTimeRecorder.mLastUpdateRequestTime = time;
+ mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime = time;
if (DEBUG) {
Log.d(TAG, "Reload request: " + mFilename + ": request=" + time + " update="
- + mSharedDictionaryController.mLastUpdateTime);
- }
- }
-
- /**
- * Reloads the dictionary if required. Reload will occur asynchronously in a separate thread.
- */
- void asyncReloadDictionaryIfRequired() {
- if (!isReloadRequired()) return;
- if (DEBUG) {
- Log.d(TAG, "Starting AsyncReloadDictionaryTask: " + mFilename);
+ + mFilenameDictionaryTimeRecorder.mLastUpdateTime);
}
- new AsyncReloadDictionaryTask().start();
}
/**
* Reloads the dictionary if required.
*/
- protected final void syncReloadDictionaryIfRequired() {
+ public final void reloadDictionaryIfRequired() {
if (!isReloadRequired()) return;
- syncReloadDictionaryInternal();
+ reloadDictionary();
}
/**
* Returns whether a dictionary reload is required.
*/
private boolean isReloadRequired() {
- return mBinaryDictionary == null || mLocalDictionaryController.isOutOfDate();
+ return mBinaryDictionary == null || mPerInstanceDictionaryTimeRecorder.isOutOfDate();
}
/**
* Reloads the dictionary. Access is controlled on a per dictionary file basis and supports
* concurrent calls from multiple instances that share the same dictionary file.
*/
- private final void syncReloadDictionaryInternal() {
+ private final void reloadDictionary() {
// Ensure that only one thread attempts to read or write to the shared binary dictionary
// file at the same time.
- mSharedDictionaryController.lock();
- try {
- final long time = SystemClock.uptimeMillis();
- final boolean dictionaryFileExists = dictionaryFileExists();
- if (mSharedDictionaryController.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.
- mSharedDictionaryController.mLastUpdateTime = time;
- generateBinaryDictionary();
+ getExecutor(mFilename).execute(new Runnable() {
+ @Override
+ public void run() {
+ final long time = SystemClock.uptimeMillis();
+ final boolean dictionaryFileExists = dictionaryFileExists();
+ if (mFilenameDictionaryTimeRecorder.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.
+ mFilenameDictionaryTimeRecorder.mLastUpdateTime = time;
+ writeBinaryDictionary();
+ loadBinaryDictionary();
+ } else {
+ // If not, the reload request was unnecessary so revert
+ // LastUpdateRequestTime to LastUpdateTime.
+ mFilenameDictionaryTimeRecorder.mLastUpdateRequestTime =
+ mFilenameDictionaryTimeRecorder.mLastUpdateTime;
+ }
+ } else if (mBinaryDictionary == null ||
+ mPerInstanceDictionaryTimeRecorder.mLastUpdateTime
+ < mFilenameDictionaryTimeRecorder.mLastUpdateTime) {
+ // Otherwise, if the local dictionary is older than the shared dictionary, load
+ // the shared dictionary.
+ loadBinaryDictionary();
+ }
+ if (mBinaryDictionary != null && !mBinaryDictionary.isValidDictionary()) {
+ // Binary dictionary is not valid. Regenerate the dictionary file.
+ mFilenameDictionaryTimeRecorder.mLastUpdateTime = time;
+ writeBinaryDictionary();
loadBinaryDictionary();
- } else {
- // If not, the reload request was unnecessary so revert LastUpdateRequestTime
- // to LastUpdateTime.
- mSharedDictionaryController.mLastUpdateRequestTime =
- mSharedDictionaryController.mLastUpdateTime;
}
- } else if (mBinaryDictionary == null || mLocalDictionaryController.mLastUpdateTime
- < mSharedDictionaryController.mLastUpdateTime) {
- // Otherwise, if the local dictionary is older than the shared dictionary, load the
- // shared dictionary.
- loadBinaryDictionary();
+ mPerInstanceDictionaryTimeRecorder.mLastUpdateTime = time;
}
- mLocalDictionaryController.mLastUpdateTime = time;
- } finally {
- mSharedDictionaryController.unlock();
- }
+ });
}
// TODO: cache the file's existence so that we avoid doing a disk access each time.
@@ -427,21 +594,38 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
/**
- * Thread class for asynchronously reloading and rewriting the binary dictionary.
+ * Load the dictionary to memory.
*/
- private class AsyncReloadDictionaryTask extends Thread {
- @Override
- public void run() {
- syncReloadDictionaryInternal();
- }
+ 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() {
+ final Runnable newTask = new Runnable() {
+ @Override
+ public void run() {
+ writeBinaryDictionary();
+ }
+ };
+ final Runnable oldTask = mUnfinishedFlushingTask.getAndSet(newTask);
+ getExecutor(mFilename).replaceAndExecute(oldTask, newTask);
}
/**
- * Lock for controlling access to a given binary dictionary and for tracking whether the
- * dictionary is out of date. Can be shared across multiple dictionary instances that access the
- * same filename.
+ * Time recorder for tracking whether the dictionary is out of date.
+ * Can be shared across multiple dictionary instances that access the same filename.
*/
- private static class DictionaryController extends ReentrantLock {
+ private static class DictionaryTimeRecorder {
private volatile long mLastUpdateTime = 0;
private volatile long mLastUpdateRequestTime = 0;
@@ -449,4 +633,75 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
return (mLastUpdateRequestTime > mLastUpdateTime);
}
}
+
+ /**
+ * Dynamically adds a word unigram to the dictionary for testing with blocking-lock.
+ */
+ @UsedForTesting
+ protected void addWordDynamicallyForTests(final String word, final String shortcutTarget,
+ final int frequency, final boolean isNotAWord) {
+ getExecutor(mFilename).executePrioritized(new Runnable() {
+ @Override
+ public void run() {
+ addWordDynamically(word, shortcutTarget, frequency, isNotAWord);
+ }
+ });
+ }
+
+ /**
+ * Dynamically adds a word bigram in the dictionary for testing with blocking-lock.
+ */
+ @UsedForTesting
+ protected void addBigramDynamicallyForTests(final String word0, final String word1,
+ final int frequency, final boolean isValid) {
+ getExecutor(mFilename).executePrioritized(new Runnable() {
+ @Override
+ public void run() {
+ addBigramDynamically(word0, word1, frequency, isValid);
+ }
+ });
+ }
+
+ /**
+ * Dynamically remove a word bigram in the dictionary for testing with blocking-lock.
+ */
+ @UsedForTesting
+ protected void removeBigramDynamicallyForTests(final String word0, final String word1) {
+ getExecutor(mFilename).executePrioritized(new Runnable() {
+ @Override
+ public void run() {
+ removeBigramDynamically(word0, word1);
+ }
+ });
+ }
+
+ // TODO: Implement native binary methods once the dynamic dictionary implementation is done.
+ @UsedForTesting
+ public boolean isInDictionaryForTests(final String word) {
+ final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>();
+ getExecutor(mFilename).executePrioritized(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)
+ .isInDictionaryForTests(word));
+ }
+ }
+ }
+ });
+ return holder.get(false, TIMEOUT_FOR_READ_OPS_IN_MILLISECONDS);
+ }
+
+ @UsedForTesting
+ public void shutdownExecutorForTests() {
+ getExecutor(mFilename).shutdown();
+ }
+
+ @UsedForTesting
+ public boolean isTerminatedForTests() {
+ return getExecutor(mFilename).isTerminated();
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index 0dabdb835..d491f988a 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -16,21 +16,23 @@
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.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
import java.util.ArrayList;
import java.util.LinkedList;
/**
- * Base class for an in-memory dictionary that can grow dynamically and can
+ * 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();
/**
@@ -38,23 +40,11 @@ public class ExpandableDictionary extends Dictionary {
*/
private static final int FULL_WORD_SCORE_MULTIPLIER = 2;
- // Bigram frequency is a fixed point number with 1 meaning 1.2 and 255 meaning 1.8.
- protected static final int BIGRAM_MAX_FREQUENCY = 255;
-
- private Context mContext;
- private char[] mWordBuilder = new char[Constants.Dictionary.MAX_WORD_LENGTH];
+ private char[] mWordBuilder = new char[Constants.DICTIONARY_MAX_WORD_LENGTH];
private int mMaxDepth;
private int mInputLength;
- private boolean mRequiresReload;
-
- private boolean mUpdatingDictionary;
-
- // Use this lock before touching mUpdatingDictionary & mRequiresDownload
- private Object mUpdatingLock = new Object();
-
private static final class Node {
- Node() {}
char mCode;
int mFrequency;
boolean mTerminal;
@@ -86,7 +76,7 @@ public class ExpandableDictionary extends Dictionary {
}
}
- protected interface NextWord {
+ public interface NextWord {
public Node getWordNode();
public int getFrequency();
public ForgettingCurveParams getFcParams();
@@ -156,52 +146,18 @@ public class ExpandableDictionary extends Dictionary {
private int[][] mCodes;
- public ExpandableDictionary(final Context context, final String dictType) {
+ public ExpandableDictionary(final String dictType) {
super(dictType);
- mContext = context;
clearDictionary();
- mCodes = new int[Constants.Dictionary.MAX_WORD_LENGTH][];
- }
-
- public void loadDictionary() {
- synchronized (mUpdatingLock) {
- startDictionaryLoadingTaskLocked();
- }
- }
-
- public void startDictionaryLoadingTaskLocked() {
- if (!mUpdatingDictionary) {
- mUpdatingDictionary = true;
- mRequiresReload = false;
- new LoadDictionaryTask().start();
- }
- }
-
- public void setRequiresReload(final boolean reload) {
- synchronized (mUpdatingLock) {
- mRequiresReload = reload;
- }
- }
-
- public boolean getRequiresReload() {
- return mRequiresReload;
- }
-
- /** Override to load your dictionary here, on a background thread. */
- public void loadDictionaryAsync() {
- // empty base implementation
- }
-
- public Context getContext() {
- return mContext;
+ mCodes = new int[Constants.DICTIONARY_MAX_WORD_LENGTH][];
}
public int getMaxWordLength() {
- return Constants.Dictionary.MAX_WORD_LENGTH;
+ return Constants.DICTIONARY_MAX_WORD_LENGTH;
}
public void addWord(final String word, final String shortcutTarget, final int frequency) {
- if (word.length() >= Constants.Dictionary.MAX_WORD_LENGTH) {
+ if (word.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH) {
return;
}
addWordRec(mRoots, word, 0, shortcutTarget, frequency, null);
@@ -230,7 +186,7 @@ public class ExpandableDictionary extends Dictionary {
childNode.mShortcutOnly = isShortcutOnly;
children.add(childNode);
}
- if (wordLength == depth + 1 && shortcutTarget != null) {
+ if (wordLength == depth + 1) {
// Terminate this word
childNode.mTerminal = true;
if (isShortcutOnly) {
@@ -254,10 +210,9 @@ public class ExpandableDictionary extends Dictionary {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
- if (reloadDictionaryIfRequired()) return null;
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
if (composer.size() > 1) {
- if (composer.size() >= Constants.Dictionary.MAX_WORD_LENGTH) {
+ if (composer.size() >= Constants.DICTIONARY_MAX_WORD_LENGTH) {
return null;
}
final ArrayList<SuggestedWordInfo> suggestions =
@@ -271,17 +226,7 @@ public class ExpandableDictionary extends Dictionary {
}
}
- // This reloads the dictionary if required, and returns whether it's currently updating its
- // contents or not.
- private boolean reloadDictionaryIfRequired() {
- synchronized (mUpdatingLock) {
- // If we need to update, start off a background task
- if (mRequiresReload) startDictionaryLoadingTaskLocked();
- return mUpdatingDictionary;
- }
- }
-
- protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes,
+ private ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes,
final String prevWordForBigrams, final ProximityInfo proximityInfo) {
final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
mInputLength = codes.size();
@@ -311,11 +256,6 @@ public class ExpandableDictionary extends Dictionary {
@Override
public synchronized boolean isValidWord(final String word) {
- synchronized (mUpdatingLock) {
- // If we need to update, start off a background task
- if (mRequiresReload) startDictionaryLoadingTaskLocked();
- if (mUpdatingDictionary) return false;
- }
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,
@@ -325,10 +265,10 @@ public class ExpandableDictionary extends Dictionary {
return (node == null) ? false : !node.mShortcutOnly;
}
- protected boolean removeBigram(final String word1, final String word2) {
+ public boolean removeBigram(final String word0, final String word1) {
// Refer to addOrSetBigram() about word1.toLowerCase()
- final Node firstWord = searchWord(mRoots, word1.toLowerCase(), 0, null);
- final Node secondWord = searchWord(mRoots, word2, 0, null);
+ 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) {
@@ -350,16 +290,17 @@ public class ExpandableDictionary extends Dictionary {
/**
* Returns the word's frequency or -1 if not found
*/
- protected int getWordFrequency(final String word) {
+ @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;
}
- protected NextWord getBigramWord(final String word1, final String word2) {
- // Refer to addOrSetBigram() about word1.toLowerCase()
- final Node firstWord = searchWord(mRoots, word1.toLowerCase(), 0, null);
- final Node secondWord = searchWord(mRoots, word2, 0, null);
+ 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;
@@ -402,7 +343,9 @@ public class ExpandableDictionary extends Dictionary {
// 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, mDictType));
+ 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) {
@@ -410,7 +353,9 @@ public class ExpandableDictionary extends Dictionary {
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, mDictType));
+ finalFreq, SuggestedWordInfo.KIND_SHORTCUT, this /* sourceDict */,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+ SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
if (suggestions.size() > Suggest.MAX_SUGGESTIONS) return false;
}
}
@@ -437,7 +382,7 @@ public class ExpandableDictionary extends Dictionary {
* @param suggestions the list in which to add suggestions
*/
// TODO: Share this routine with the native code for BinaryDictionary
- protected void getWordsRec(final NodeArray roots, final WordComposer codes, final char[] word,
+ 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;
@@ -530,37 +475,41 @@ public class ExpandableDictionary extends Dictionary {
}
}
- public int setBigramAndGetFrequency(final String word1, final String word2,
+ public int setBigramAndGetFrequency(final String word0, final String word1,
final int frequency) {
- return setBigramAndGetFrequency(word1, word2, frequency, null /* unused */);
+ return setBigramAndGetFrequency(word0, word1, frequency, null /* unused */);
}
- public int setBigramAndGetFrequency(final String word1, final String word2,
+ public int setBigramAndGetFrequency(final String word0, final String word1,
final ForgettingCurveParams fcp) {
- return setBigramAndGetFrequency(word1, word2, 0 /* unused */, 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 word1 the first word of this bigram
- * @param word2 the second word of this bigram
+ * @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 word1, final String word2,
+ 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 word1Lower = word1.toLowerCase();
- if (TextUtils.isEmpty(word1Lower) || TextUtils.isEmpty(word2)) {
- Log.e(TAG, "Invalid bigram pair: " + word1 + ", " + word1Lower + ", " + word2);
+ 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, word1Lower, 0, null);
- final Node secondWord = searchWord(mRoots, word2, 0, null);
+ 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();
@@ -628,7 +577,7 @@ public class ExpandableDictionary extends Dictionary {
}
// Local to reverseLookUp, but do not allocate each time.
- private final char[] mLookedUpString = new char[Constants.Dictionary.MAX_WORD_LENGTH];
+ 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
@@ -643,7 +592,7 @@ public class ExpandableDictionary extends Dictionary {
for (NextWord nextWord : terminalNodes) {
node = nextWord.getWordNode();
freq = nextWord.getFrequency();
- int index = Constants.Dictionary.MAX_WORD_LENGTH;
+ int index = Constants.DICTIONARY_MAX_WORD_LENGTH;
do {
--index;
mLookedUpString[index] = node.mCode;
@@ -655,8 +604,10 @@ public class ExpandableDictionary extends Dictionary {
// 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, mDictType));
+ Constants.DICTIONARY_MAX_WORD_LENGTH - index),
+ freq, SuggestedWordInfo.KIND_CORRECTION, this /* sourceDict */,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+ SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
}
}
}
@@ -694,21 +645,10 @@ public class ExpandableDictionary extends Dictionary {
return null;
}
- protected void clearDictionary() {
+ public void clearDictionary() {
mRoots = new NodeArray();
}
- private final class LoadDictionaryTask extends Thread {
- LoadDictionaryTask() {}
- @Override
- public void run() {
- loadDictionaryAsync();
- synchronized (mUpdatingLock) {
- mUpdatingDictionary = false;
- }
- }
- }
-
private static char toLowerCase(final char c) {
char baseChar = c;
if (c < BASE_CHARS.length) {
@@ -727,172 +667,206 @@ public class ExpandableDictionary extends Dictionary {
* 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[] = {
- 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
- 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f,
- 0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
- 0x0018, 0x0019, 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f,
- 0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
- 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f,
- 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
- 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f,
- 0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
- 0x0048, 0x0049, 0x004a, 0x004b, 0x004c, 0x004d, 0x004e, 0x004f,
- 0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
- 0x0058, 0x0059, 0x005a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f,
- 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
- 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f,
- 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
- 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x007f,
- 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087,
- 0x0088, 0x0089, 0x008a, 0x008b, 0x008c, 0x008d, 0x008e, 0x008f,
- 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097,
- 0x0098, 0x0099, 0x009a, 0x009b, 0x009c, 0x009d, 0x009e, 0x009f,
- 0x0020, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7,
- 0x0020, 0x00a9, 0x0061, 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x0020,
- 0x00b0, 0x00b1, 0x0032, 0x0033, 0x0020, 0x03bc, 0x00b6, 0x00b7,
- 0x0020, 0x0031, 0x006f, 0x00bb, 0x0031, 0x0031, 0x0033, 0x00bf,
- 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x0041, 0x00c6, 0x0043,
- 0x0045, 0x0045, 0x0045, 0x0045, 0x0049, 0x0049, 0x0049, 0x0049,
- 0x00d0, 0x004e, 0x004f, 0x004f, 0x004f, 0x004f, 0x004f, 0x00d7,
- 0x004f, 0x0055, 0x0055, 0x0055, 0x0055, 0x0059, 0x00de, 0x0073, // Manually changed d8 to 4f
- // Manually changed df to 73
- 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x0063,
- 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069,
- 0x00f0, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x00f7,
- 0x006f, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00fe, 0x0079, // Manually changed f8 to 6f
- 0x0041, 0x0061, 0x0041, 0x0061, 0x0041, 0x0061, 0x0043, 0x0063,
- 0x0043, 0x0063, 0x0043, 0x0063, 0x0043, 0x0063, 0x0044, 0x0064,
- 0x0110, 0x0111, 0x0045, 0x0065, 0x0045, 0x0065, 0x0045, 0x0065,
- 0x0045, 0x0065, 0x0045, 0x0065, 0x0047, 0x0067, 0x0047, 0x0067,
- 0x0047, 0x0067, 0x0047, 0x0067, 0x0048, 0x0068, 0x0126, 0x0127,
- 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069, 0x0049, 0x0069,
- 0x0049, 0x0131, 0x0049, 0x0069, 0x004a, 0x006a, 0x004b, 0x006b,
- 0x0138, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c, 0x006c, 0x004c,
- 0x006c, 0x0141, 0x0142, 0x004e, 0x006e, 0x004e, 0x006e, 0x004e,
- 0x006e, 0x02bc, 0x014a, 0x014b, 0x004f, 0x006f, 0x004f, 0x006f,
- 0x004f, 0x006f, 0x0152, 0x0153, 0x0052, 0x0072, 0x0052, 0x0072,
- 0x0052, 0x0072, 0x0053, 0x0073, 0x0053, 0x0073, 0x0053, 0x0073,
- 0x0053, 0x0073, 0x0054, 0x0074, 0x0054, 0x0074, 0x0166, 0x0167,
- 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075, 0x0055, 0x0075,
- 0x0055, 0x0075, 0x0055, 0x0075, 0x0057, 0x0077, 0x0059, 0x0079,
- 0x0059, 0x005a, 0x007a, 0x005a, 0x007a, 0x005a, 0x007a, 0x0073,
- 0x0180, 0x0181, 0x0182, 0x0183, 0x0184, 0x0185, 0x0186, 0x0187,
- 0x0188, 0x0189, 0x018a, 0x018b, 0x018c, 0x018d, 0x018e, 0x018f,
- 0x0190, 0x0191, 0x0192, 0x0193, 0x0194, 0x0195, 0x0196, 0x0197,
- 0x0198, 0x0199, 0x019a, 0x019b, 0x019c, 0x019d, 0x019e, 0x019f,
- 0x004f, 0x006f, 0x01a2, 0x01a3, 0x01a4, 0x01a5, 0x01a6, 0x01a7,
- 0x01a8, 0x01a9, 0x01aa, 0x01ab, 0x01ac, 0x01ad, 0x01ae, 0x0055,
- 0x0075, 0x01b1, 0x01b2, 0x01b3, 0x01b4, 0x01b5, 0x01b6, 0x01b7,
- 0x01b8, 0x01b9, 0x01ba, 0x01bb, 0x01bc, 0x01bd, 0x01be, 0x01bf,
- 0x01c0, 0x01c1, 0x01c2, 0x01c3, 0x0044, 0x0044, 0x0064, 0x004c,
- 0x004c, 0x006c, 0x004e, 0x004e, 0x006e, 0x0041, 0x0061, 0x0049,
- 0x0069, 0x004f, 0x006f, 0x0055, 0x0075, 0x00dc, 0x00fc, 0x00dc,
- 0x00fc, 0x00dc, 0x00fc, 0x00dc, 0x00fc, 0x01dd, 0x00c4, 0x00e4,
- 0x0226, 0x0227, 0x00c6, 0x00e6, 0x01e4, 0x01e5, 0x0047, 0x0067,
- 0x004b, 0x006b, 0x004f, 0x006f, 0x01ea, 0x01eb, 0x01b7, 0x0292,
- 0x006a, 0x0044, 0x0044, 0x0064, 0x0047, 0x0067, 0x01f6, 0x01f7,
- 0x004e, 0x006e, 0x00c5, 0x00e5, 0x00c6, 0x00e6, 0x00d8, 0x00f8,
- 0x0041, 0x0061, 0x0041, 0x0061, 0x0045, 0x0065, 0x0045, 0x0065,
- 0x0049, 0x0069, 0x0049, 0x0069, 0x004f, 0x006f, 0x004f, 0x006f,
- 0x0052, 0x0072, 0x0052, 0x0072, 0x0055, 0x0075, 0x0055, 0x0075,
- 0x0053, 0x0073, 0x0054, 0x0074, 0x021c, 0x021d, 0x0048, 0x0068,
- 0x0220, 0x0221, 0x0222, 0x0223, 0x0224, 0x0225, 0x0041, 0x0061,
- 0x0045, 0x0065, 0x00d6, 0x00f6, 0x00d5, 0x00f5, 0x004f, 0x006f,
- 0x022e, 0x022f, 0x0059, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237,
- 0x0238, 0x0239, 0x023a, 0x023b, 0x023c, 0x023d, 0x023e, 0x023f,
- 0x0240, 0x0241, 0x0242, 0x0243, 0x0244, 0x0245, 0x0246, 0x0247,
- 0x0248, 0x0249, 0x024a, 0x024b, 0x024c, 0x024d, 0x024e, 0x024f,
- 0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257,
- 0x0258, 0x0259, 0x025a, 0x025b, 0x025c, 0x025d, 0x025e, 0x025f,
- 0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267,
- 0x0268, 0x0269, 0x026a, 0x026b, 0x026c, 0x026d, 0x026e, 0x026f,
- 0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277,
- 0x0278, 0x0279, 0x027a, 0x027b, 0x027c, 0x027d, 0x027e, 0x027f,
- 0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287,
- 0x0288, 0x0289, 0x028a, 0x028b, 0x028c, 0x028d, 0x028e, 0x028f,
- 0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297,
- 0x0298, 0x0299, 0x029a, 0x029b, 0x029c, 0x029d, 0x029e, 0x029f,
- 0x02a0, 0x02a1, 0x02a2, 0x02a3, 0x02a4, 0x02a5, 0x02a6, 0x02a7,
- 0x02a8, 0x02a9, 0x02aa, 0x02ab, 0x02ac, 0x02ad, 0x02ae, 0x02af,
- 0x0068, 0x0266, 0x006a, 0x0072, 0x0279, 0x027b, 0x0281, 0x0077,
- 0x0079, 0x02b9, 0x02ba, 0x02bb, 0x02bc, 0x02bd, 0x02be, 0x02bf,
- 0x02c0, 0x02c1, 0x02c2, 0x02c3, 0x02c4, 0x02c5, 0x02c6, 0x02c7,
- 0x02c8, 0x02c9, 0x02ca, 0x02cb, 0x02cc, 0x02cd, 0x02ce, 0x02cf,
- 0x02d0, 0x02d1, 0x02d2, 0x02d3, 0x02d4, 0x02d5, 0x02d6, 0x02d7,
- 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02de, 0x02df,
- 0x0263, 0x006c, 0x0073, 0x0078, 0x0295, 0x02e5, 0x02e6, 0x02e7,
- 0x02e8, 0x02e9, 0x02ea, 0x02eb, 0x02ec, 0x02ed, 0x02ee, 0x02ef,
- 0x02f0, 0x02f1, 0x02f2, 0x02f3, 0x02f4, 0x02f5, 0x02f6, 0x02f7,
- 0x02f8, 0x02f9, 0x02fa, 0x02fb, 0x02fc, 0x02fd, 0x02fe, 0x02ff,
- 0x0300, 0x0301, 0x0302, 0x0303, 0x0304, 0x0305, 0x0306, 0x0307,
- 0x0308, 0x0309, 0x030a, 0x030b, 0x030c, 0x030d, 0x030e, 0x030f,
- 0x0310, 0x0311, 0x0312, 0x0313, 0x0314, 0x0315, 0x0316, 0x0317,
- 0x0318, 0x0319, 0x031a, 0x031b, 0x031c, 0x031d, 0x031e, 0x031f,
- 0x0320, 0x0321, 0x0322, 0x0323, 0x0324, 0x0325, 0x0326, 0x0327,
- 0x0328, 0x0329, 0x032a, 0x032b, 0x032c, 0x032d, 0x032e, 0x032f,
- 0x0330, 0x0331, 0x0332, 0x0333, 0x0334, 0x0335, 0x0336, 0x0337,
- 0x0338, 0x0339, 0x033a, 0x033b, 0x033c, 0x033d, 0x033e, 0x033f,
- 0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x0345, 0x0346, 0x0347,
- 0x0348, 0x0349, 0x034a, 0x034b, 0x034c, 0x034d, 0x034e, 0x034f,
- 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357,
- 0x0358, 0x0359, 0x035a, 0x035b, 0x035c, 0x035d, 0x035e, 0x035f,
- 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367,
- 0x0368, 0x0369, 0x036a, 0x036b, 0x036c, 0x036d, 0x036e, 0x036f,
- 0x0370, 0x0371, 0x0372, 0x0373, 0x02b9, 0x0375, 0x0376, 0x0377,
- 0x0378, 0x0379, 0x0020, 0x037b, 0x037c, 0x037d, 0x003b, 0x037f,
- 0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x00a8, 0x0391, 0x00b7,
- 0x0395, 0x0397, 0x0399, 0x038b, 0x039f, 0x038d, 0x03a5, 0x03a9,
- 0x03ca, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397,
- 0x0398, 0x0399, 0x039a, 0x039b, 0x039c, 0x039d, 0x039e, 0x039f,
- 0x03a0, 0x03a1, 0x03a2, 0x03a3, 0x03a4, 0x03a5, 0x03a6, 0x03a7,
- 0x03a8, 0x03a9, 0x0399, 0x03a5, 0x03b1, 0x03b5, 0x03b7, 0x03b9,
- 0x03cb, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7,
- 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf,
- 0x03c0, 0x03c1, 0x03c2, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7,
- 0x03c8, 0x03c9, 0x03b9, 0x03c5, 0x03bf, 0x03c5, 0x03c9, 0x03cf,
- 0x03b2, 0x03b8, 0x03a5, 0x03d2, 0x03d2, 0x03c6, 0x03c0, 0x03d7,
- 0x03d8, 0x03d9, 0x03da, 0x03db, 0x03dc, 0x03dd, 0x03de, 0x03df,
- 0x03e0, 0x03e1, 0x03e2, 0x03e3, 0x03e4, 0x03e5, 0x03e6, 0x03e7,
- 0x03e8, 0x03e9, 0x03ea, 0x03eb, 0x03ec, 0x03ed, 0x03ee, 0x03ef,
- 0x03ba, 0x03c1, 0x03c2, 0x03f3, 0x0398, 0x03b5, 0x03f6, 0x03f7,
- 0x03f8, 0x03a3, 0x03fa, 0x03fb, 0x03fc, 0x03fd, 0x03fe, 0x03ff,
- 0x0415, 0x0415, 0x0402, 0x0413, 0x0404, 0x0405, 0x0406, 0x0406,
- 0x0408, 0x0409, 0x040a, 0x040b, 0x041a, 0x0418, 0x0423, 0x040f,
- 0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
- 0x0418, 0x0418, 0x041a, 0x041b, 0x041c, 0x041d, 0x041e, 0x041f,
- 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
- 0x0428, 0x0429, 0x042a, 0x042b, 0x042c, 0x042d, 0x042e, 0x042f,
- 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
- 0x0438, 0x0438, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f,
- 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
- 0x0448, 0x0449, 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f,
- 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456,
- 0x0458, 0x0459, 0x045a, 0x045b, 0x043a, 0x0438, 0x0443, 0x045f,
- 0x0460, 0x0461, 0x0462, 0x0463, 0x0464, 0x0465, 0x0466, 0x0467,
- 0x0468, 0x0469, 0x046a, 0x046b, 0x046c, 0x046d, 0x046e, 0x046f,
- 0x0470, 0x0471, 0x0472, 0x0473, 0x0474, 0x0475, 0x0474, 0x0475,
- 0x0478, 0x0479, 0x047a, 0x047b, 0x047c, 0x047d, 0x047e, 0x047f,
- 0x0480, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487,
- 0x0488, 0x0489, 0x048a, 0x048b, 0x048c, 0x048d, 0x048e, 0x048f,
- 0x0490, 0x0491, 0x0492, 0x0493, 0x0494, 0x0495, 0x0496, 0x0497,
- 0x0498, 0x0499, 0x049a, 0x049b, 0x049c, 0x049d, 0x049e, 0x049f,
- 0x04a0, 0x04a1, 0x04a2, 0x04a3, 0x04a4, 0x04a5, 0x04a6, 0x04a7,
- 0x04a8, 0x04a9, 0x04aa, 0x04ab, 0x04ac, 0x04ad, 0x04ae, 0x04af,
- 0x04b0, 0x04b1, 0x04b2, 0x04b3, 0x04b4, 0x04b5, 0x04b6, 0x04b7,
- 0x04b8, 0x04b9, 0x04ba, 0x04bb, 0x04bc, 0x04bd, 0x04be, 0x04bf,
- 0x04c0, 0x0416, 0x0436, 0x04c3, 0x04c4, 0x04c5, 0x04c6, 0x04c7,
- 0x04c8, 0x04c9, 0x04ca, 0x04cb, 0x04cc, 0x04cd, 0x04ce, 0x04cf,
- 0x0410, 0x0430, 0x0410, 0x0430, 0x04d4, 0x04d5, 0x0415, 0x0435,
- 0x04d8, 0x04d9, 0x04d8, 0x04d9, 0x0416, 0x0436, 0x0417, 0x0437,
- 0x04e0, 0x04e1, 0x0418, 0x0438, 0x0418, 0x0438, 0x041e, 0x043e,
- 0x04e8, 0x04e9, 0x04e8, 0x04e9, 0x042d, 0x044d, 0x0423, 0x0443,
- 0x0423, 0x0443, 0x0423, 0x0443, 0x0427, 0x0447, 0x04f6, 0x04f7,
- 0x042b, 0x044b, 0x04fa, 0x04fb, 0x04fc, 0x04fd, 0x04fe, 0x04ff,
+ /* 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,
};
-
- // generated with:
- // cat UnicodeData.txt | perl -e 'while (<>) { @foo = split(/;/); $foo[5] =~ s/<.*> //; $base[hex($foo[0])] = hex($foo[5]);} for ($i = 0; $i < 0x500; $i += 8) { for ($j = $i; $j < $i + 8; $j++) { printf("0x%04x, ", $base[$j] ? $base[$j] : $j)}; print "\n"; }'
-
}
diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java
index 1f673e9b0..21b103e5a 100644
--- a/java/src/com/android/inputmethod/latin/InputAttributes.java
+++ b/java/src/com/android/inputmethod/latin/InputAttributes.java
@@ -20,6 +20,9 @@ import android.text.InputType;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
+import com.android.inputmethod.latin.utils.InputTypeUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
+
/**
* Class to hold attributes of the input field.
*/
diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java
index 81c833000..2e638aaf3 100644
--- a/java/src/com/android/inputmethod/latin/InputPointers.java
+++ b/java/src/com/android/inputmethod/latin/InputPointers.java
@@ -17,6 +17,7 @@
package com.android.inputmethod.latin;
import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.utils.ResizableIntArray;
import android.util.Log;
@@ -104,6 +105,17 @@ public final class InputPointers {
mTimes.append(times, startPos, length);
}
+ /**
+ * Shift to the left by elementCount, discarding elementCount pointers at the start.
+ * @param elementCount how many elements to shift.
+ */
+ public void shift(final int elementCount) {
+ mXCoordinates.shift(elementCount);
+ mYCoordinates.shift(elementCount);
+ mPointerIds.shift(elementCount);
+ mTimes.shift(elementCount);
+ }
+
public void reset() {
final int defaultCapacity = mDefaultCapacity;
mXCoordinates.reset(defaultCapacity);
diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java
index 5359c8185..81ccf83d8 100644
--- a/java/src/com/android/inputmethod/latin/InputView.java
+++ b/java/src/com/android/inputmethod/latin/InputView.java
@@ -24,7 +24,7 @@ import android.view.View;
import android.widget.LinearLayout;
public final class InputView extends LinearLayout {
- private View mSuggestionStripContainer;
+ private View mSuggestionStripView;
private View mKeyboardView;
private int mKeyboardTopPadding;
@@ -33,33 +33,29 @@ public final class InputView extends LinearLayout {
private final Rect mEventForwardingRect = new Rect();
private final Rect mEventReceivingRect = new Rect();
- public InputView(Context context, AttributeSet attrs) {
+ public InputView(final Context context, final AttributeSet attrs) {
super(context, attrs, 0);
}
- public void setKeyboardGeometry(int keyboardTopPadding) {
+ public void setKeyboardGeometry(final int keyboardTopPadding) {
mKeyboardTopPadding = keyboardTopPadding;
}
@Override
protected void onFinishInflate() {
- mSuggestionStripContainer = findViewById(R.id.suggestions_container);
+ mSuggestionStripView = findViewById(R.id.suggestion_strip_view);
mKeyboardView = findViewById(R.id.keyboard_view);
}
@Override
- public boolean dispatchTouchEvent(MotionEvent me) {
- if (mSuggestionStripContainer.getVisibility() == VISIBLE
- && mKeyboardView.getVisibility() == VISIBLE
- && forwardTouchEvent(me)) {
- return true;
+ public boolean dispatchTouchEvent(final MotionEvent me) {
+ if (mSuggestionStripView.getVisibility() != VISIBLE
+ || mKeyboardView.getVisibility() != VISIBLE) {
+ return super.dispatchTouchEvent(me);
}
- return super.dispatchTouchEvent(me);
- }
- // The touch events that hit the top padding of keyboard should be forwarded to
- // {@link SuggestionStripView}.
- private boolean forwardTouchEvent(MotionEvent me) {
+ // The touch events that hit the top padding of keyboard should be forwarded to
+ // {@link SuggestionStripView}.
final Rect rect = mInputViewRect;
this.getGlobalVisibleRect(rect);
final int x = (int)me.getX() + rect.left;
@@ -68,7 +64,7 @@ public final class InputView extends LinearLayout {
final Rect forwardingRect = mEventForwardingRect;
mKeyboardView.getGlobalVisibleRect(forwardingRect);
if (!mIsForwardingEvent && !forwardingRect.contains(x, y)) {
- return false;
+ return super.dispatchTouchEvent(me);
}
final int forwardingLimitY = forwardingRect.top + mKeyboardTopPadding;
@@ -93,11 +89,11 @@ public final class InputView extends LinearLayout {
}
if (!sendToTarget) {
- return false;
+ return super.dispatchTouchEvent(me);
}
final Rect receivingRect = mEventReceivingRect;
- mSuggestionStripContainer.getGlobalVisibleRect(receivingRect);
+ mSuggestionStripView.getGlobalVisibleRect(receivingRect);
final int translatedX = x - receivingRect.left;
final int translatedY;
if (y < forwardingLimitY) {
@@ -107,7 +103,7 @@ public final class InputView extends LinearLayout {
translatedY = y - receivingRect.top;
}
me.setLocation(translatedX, translatedY);
- mSuggestionStripContainer.dispatchTouchEvent(me);
+ mSuggestionStripView.dispatchTouchEvent(me);
return true;
}
}
diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java
index 826dc11e7..642b3a4da 100644
--- a/java/src/com/android/inputmethod/latin/LastComposedWord.java
+++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java
@@ -16,6 +16,8 @@
package com.android.inputmethod.latin;
+import com.android.inputmethod.latin.utils.StringUtils;
+
import android.text.TextUtils;
/**
@@ -47,7 +49,7 @@ public final class LastComposedWord {
public final String mPrevWord;
public final int mCapitalizedMode;
public final InputPointers mInputPointers =
- new InputPointers(Constants.Dictionary.MAX_WORD_LENGTH);
+ new InputPointers(Constants.DICTIONARY_MAX_WORD_LENGTH);
private boolean mActive;
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 0bf167fd4..270dc4c06 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -43,10 +43,10 @@ import android.os.Message;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.text.InputType;
-import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.SuggestionSpan;
import android.util.Log;
+import android.util.Pair;
import android.util.PrintWriterPrinter;
import android.util.Printer;
import android.view.KeyCharacterMap;
@@ -74,11 +74,33 @@ import com.android.inputmethod.keyboard.KeyboardActionListener;
import com.android.inputmethod.keyboard.KeyboardId;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.keyboard.MainKeyboardView;
-import com.android.inputmethod.latin.RichInputConnection.Range;
+import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.Utils.Stats;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
+import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister;
+import com.android.inputmethod.latin.personalization.PersonalizationHelper;
+import com.android.inputmethod.latin.personalization.PersonalizationPredictionDictionary;
+import com.android.inputmethod.latin.personalization.UserHistoryPredictionDictionary;
+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.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.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.TargetPackageInfoGetterTask;
+import com.android.inputmethod.latin.utils.TextRange;
+import com.android.inputmethod.latin.utils.UserHistoryForgettingCurveUtils;
import com.android.inputmethod.research.ResearchLogger;
import java.io.FileDescriptor;
@@ -106,6 +128,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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.
@@ -135,11 +162,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private View mExtractArea;
private View mKeyPreviewBackingView;
- private View mSuggestionsContainer;
private SuggestionStripView mSuggestionStripView;
// Never null
private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
- @UsedForTesting Suggest mSuggest;
+ private Suggest mSuggest;
private CompletionInfo[] mApplicationSpecifiedCompletions;
private AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils();
@@ -153,12 +179,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private boolean mIsMainDictionaryAvailable;
private UserBinaryDictionary mUserDictionary;
- private UserHistoryDictionary mUserHistoryDictionary;
+ private UserHistoryPredictionDictionary mUserHistoryPredictionDictionary;
+ private PersonalizationPredictionDictionary mPersonalizationPredictionDictionary;
+ private PersonalizationDictionary mPersonalizationDictionary;
private boolean mIsUserDictionaryAvailable;
private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
- private PositionalInfoForUserDictPendingAddition
- mPositionalInfoForUserDictPendingAddition = null;
private final WordComposer mWordComposer = new WordComposer();
private final RichInputConnection mConnection = new RichInputConnection(this);
private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
@@ -173,17 +199,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private boolean mExpectingUpdateSelection;
private int mDeleteCount;
private long mLastKeyTime;
- private TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
+ 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.
- // TODO: The development-only-diagnostic version is not supported by the Dictionary Pack
- // Service yet.
private BroadcastReceiver mDictionaryPackInstallReceiver =
- ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS
- ? null : new DictionaryPackInstallBroadcastReceiver(this);
+ new DictionaryPackInstallBroadcastReceiver(this);
// Keeps track of most recently inserted text (multi-character key) for reverting
private String mEnteredText;
@@ -197,6 +223,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private final boolean mIsHardwareAcceleratedDrawingEnabled;
public final UIHandler mHandler = new UIHandler(this);
+ private InputUpdater mInputUpdater;
public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
private static final int MSG_UPDATE_SHIFT_STATE = 0;
@@ -204,8 +231,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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_RESET_CACHES = 7;
+ 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 int mDelayUpdateSuggestions;
private int mDelayUpdateShiftState;
@@ -238,12 +272,36 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
switcher.updateShiftState();
break;
case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
- latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj,
- msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
+ 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);
+ }
+ } else {
+ latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords) msg.obj,
+ msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
+ }
break;
case MSG_RESUME_SUGGESTIONS:
latinIme.restartSuggestionsOnWordTouchedByCursor();
break;
+ case MSG_REOPEN_DICTIONARIES:
+ latinIme.initSuggest();
+ // 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);
+ break;
+ case MSG_RESET_CACHES:
+ latinIme.retryResetCaches(msg.arg1 == 1 /* tryResumeSuggestions */,
+ msg.arg2 /* remainingTries */);
+ break;
}
}
@@ -251,11 +309,21 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
}
+ public void postReopenDictionaries() {
+ sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES));
+ }
+
public void postResumeSuggestions() {
removeMessages(MSG_RESUME_SUGGESTIONS);
sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
}
+ public void postResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
+ removeMessages(MSG_RESET_CACHES);
+ sendMessage(obtainMessage(MSG_RESET_CACHES, tryResumeSuggestions ? 1 : 0,
+ remainingTries, null));
+ }
+
public void cancelUpdateSuggestionStrip() {
removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
}
@@ -264,6 +332,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return hasMessages(MSG_UPDATE_SUGGESTION_STRIP);
}
+ public boolean hasPendingReopenDictionaries() {
+ return hasMessages(MSG_REOPEN_DICTIONARIES);
+ }
+
public void postUpdateShiftState() {
removeMessages(MSG_UPDATE_SHIFT_STATE);
sendMessageDelayed(obtainMessage(MSG_UPDATE_SHIFT_STATE), mDelayUpdateShiftState);
@@ -277,9 +349,29 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final boolean dismissGestureFloatingPreviewText) {
removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
final int arg1 = dismissGestureFloatingPreviewText
- ? ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT : 0;
- obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, arg1, 0, suggestedWords)
- .sendToTarget();
+ ? 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();
+ }
+
+ 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();
+ }
+
+ // 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() {
@@ -416,6 +508,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
+ // Loading the native library eagerly to avoid unexpected UnsatisfiedLinkError at the initial
+ // JNI call as much as possible.
+ static {
+ JniUtils.loadNativeLibrary();
+ }
+
public LatinIME() {
super();
mSettings = Settings.getInstance();
@@ -436,6 +534,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
KeyboardSwitcher.init(this);
AudioAndHapticFeedbackManager.init(this);
AccessibilityUtils.init(this);
+ PersonalizationDictionarySessionRegister.init(this);
super.onCreate();
@@ -458,19 +557,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
registerReceiver(mReceiver, filter);
- // TODO: The development-only-diagnostic version is not supported by the Dictionary Pack
- // Service yet.
- if (!ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- final IntentFilter packageFilter = new IntentFilter();
- packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
- packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
- packageFilter.addDataScheme(SCHEME_PACKAGE);
- registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
+ final IntentFilter packageFilter = new IntentFilter();
+ packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ packageFilter.addDataScheme(SCHEME_PACKAGE);
+ registerReceiver(mDictionaryPackInstallReceiver, packageFilter);
- final IntentFilter newDictFilter = new IntentFilter();
- newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
- registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
- }
+ final IntentFilter newDictFilter = new IntentFilter();
+ newDictFilter.addAction(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
+ registerReceiver(mDictionaryPackInstallReceiver, newDictFilter);
+
+ mInputUpdater = new InputUpdater(this);
}
// Has to be package-visible for unit tests
@@ -481,8 +578,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
mSettings.loadSettings(locale, inputAttributes);
AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent());
- // May need to reset the contacts dictionary depending on the user settings.
- resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
+ // 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.
+ if (!mHandler.hasPendingReopenDictionaries()) {
+ // May need to reset the contacts dictionary depending on the user settings.
+ resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
+ }
}
// Note that this method is called from a non-UI thread.
@@ -499,33 +605,38 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
final String localeStr = subtypeLocale.toString();
- final ContactsBinaryDictionary oldContactsDictionary;
- if (mSuggest != null) {
- oldContactsDictionary = mSuggest.getContactsDictionary();
- mSuggest.close();
- } else {
- oldContactsDictionary = null;
- }
- mSuggest = new Suggest(this /* Context */, subtypeLocale,
+ final Suggest newSuggest = new Suggest(this /* Context */, subtypeLocale,
this /* SuggestInitializationListener */);
- if (mSettings.getCurrent().mCorrectionEnabled) {
- mSuggest.setAutoCorrectionThreshold(mSettings.getCurrent().mAutoCorrectionThreshold);
+ 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(mSuggest);
+ ResearchLogger.getInstance().initSuggest(newSuggest);
}
mUserDictionary = new UserBinaryDictionary(this, localeStr);
mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
- mSuggest.setUserDictionary(mUserDictionary);
-
- resetContactsDictionary(oldContactsDictionary);
+ newSuggest.setUserDictionary(mUserDictionary);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- mUserHistoryDictionary = UserHistoryDictionary.getInstance(this, localeStr, prefs);
- mSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
+
+ mUserHistoryPredictionDictionary = PersonalizationHelper
+ .getUserHistoryPredictionDictionary(this, localeStr, prefs);
+ newSuggest.setUserHistoryPredictionDictionary(mUserHistoryPredictionDictionary);
+ 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();
}
/**
@@ -537,8 +648,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
* @param oldContactsDictionary an optional dictionary to use, or null
*/
private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
+ final Suggest suggest = mSuggest;
final boolean shouldSetDictionary =
- (null != mSuggest && mSettings.getCurrent().mUseContactsDict);
+ (null != suggest && mSettings.getCurrent().mUseContactsDict);
final ContactsBinaryDictionary dictionaryToUse;
if (!shouldSetDictionary) {
@@ -565,8 +677,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- if (null != mSuggest) {
- mSuggest.setContactsDictionary(dictionaryToUse);
+ if (null != suggest) {
+ suggest.setContactsDictionary(dictionaryToUse);
}
}
@@ -578,8 +690,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onDestroy() {
- if (mSuggest != null) {
- mSuggest.close();
+ final Suggest suggest = mSuggest;
+ if (suggest != null) {
+ suggest.close();
mSuggest = null;
}
mSettings.onDestroy();
@@ -587,13 +700,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.getInstance().onDestroy();
}
- // TODO: The development-only-diagnostic version is not supported by the Dictionary Pack
- // Service yet.
- if (!ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- unregisterReceiver(mDictionaryPackInstallReceiver);
- }
+ unregisterReceiver(mDictionaryPackInstallReceiver);
+ PersonalizationDictionarySessionRegister.onDestroy(this);
LatinImeLogger.commit();
LatinImeLogger.onDestroy();
+ if (mInputUpdater != null) {
+ mInputUpdater.onDestroy();
+ mInputUpdater = null;
+ }
super.onDestroy();
}
@@ -611,6 +725,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mOptionsDialog.dismiss();
}
}
+ PersonalizationDictionarySessionRegister.onConfigurationChanged(this, conf);
super.onConfigurationChanged(conf);
}
@@ -625,7 +740,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mExtractArea = getWindow().getWindow().getDecorView()
.findViewById(android.R.id.extractArea);
mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
- mSuggestionsContainer = view.findViewById(R.id.suggestions_container);
mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
if (mSuggestionStripView != null)
mSuggestionStripView.setListener(this, view);
@@ -677,7 +791,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
super.onStartInputView(editorInfo, restarting);
final KeyboardSwitcher switcher = mKeyboardSwitcher;
final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
- final SettingsValues currentSettings = mSettings.getCurrent();
+ // If we are starting input in a different text field from before, we'll have to reload
+ // settings, so currentSettingsValues can't be final.
+ SettingsValues currentSettingsValues = mSettings.getCurrent();
if (editorInfo == null) {
Log.e(TAG, "Null EditorInfo in onStartInputView()");
@@ -732,7 +848,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting);
}
- final boolean inputTypeChanged = !currentSettings.isSameInputType(editorInfo);
+ final boolean inputTypeChanged = !currentSettingsValues.isSameInputType(editorInfo);
final boolean isDifferentTextField = !restarting || inputTypeChanged;
if (isDifferentTextField) {
mSubtypeSwitcher.updateParametersOnStartInputView();
@@ -754,7 +870,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Note: the following does a round-trip IPC on the main thread: be careful
final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
- if (null != mSuggest && null != currentLocale && !currentLocale.equals(mSuggest.mLocale)) {
+ final Suggest suggest = mSuggest;
+ if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) {
initSuggest();
}
if (mSuggestionStripView != null) {
@@ -764,18 +881,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
mSuggestedWords = SuggestedWords.EMPTY;
- mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart,
- 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.
+ if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(editorInfo.initialSelStart,
+ false /* shouldFinishComposition */)) {
+ // We try resetting the caches up to 5 times before giving up.
+ mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */);
+ } else {
+ if (isDifferentTextField) mHandler.postResumeSuggestions();
+ }
if (isDifferentTextField) {
mainKeyboardView.closing();
loadSettings();
+ currentSettingsValues = mSettings.getCurrent();
- if (mSuggest != null && currentSettings.mCorrectionEnabled) {
- mSuggest.setAutoCorrectionThreshold(currentSettings.mAutoCorrectionThreshold);
+ if (suggest != null && currentSettingsValues.mCorrectionEnabled) {
+ suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold);
}
- switcher.loadKeyboard(editorInfo, currentSettings);
+ switcher.loadKeyboard(editorInfo, currentSettingsValues);
} 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.
@@ -791,36 +917,81 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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();
mHandler.cancelUpdateSuggestionStrip();
mHandler.cancelDoubleSpacePeriodTimer();
mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable);
- mainKeyboardView.setKeyPreviewPopupEnabled(currentSettings.mKeyPreviewPopupOn,
- currentSettings.mKeyPreviewPopupDismissDelay);
+ mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn,
+ currentSettingsValues.mKeyPreviewPopupDismissDelay);
mainKeyboardView.setSlidingKeyInputPreviewEnabled(
- currentSettings.mSlidingKeyInputPreviewEnabled);
+ currentSettingsValues.mSlidingKeyInputPreviewEnabled);
mainKeyboardView.setGestureHandlingEnabledByUser(
- currentSettings.mGestureInputEnabled);
- mainKeyboardView.setGesturePreviewMode(currentSettings.mGesturePreviewTrailEnabled,
- currentSettings.mGestureFloatingPreviewTextEnabled);
-
- // If we have a user dictionary addition in progress, we should check now if we should
- // replace the previously committed string with the word that has actually been added
- // to the user dictionary.
- if (null != mPositionalInfoForUserDictPendingAddition
- && mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord(
- mConnection, editorInfo, mLastSelectionEnd, currentLocale)) {
- mPositionalInfoForUserDictPendingAddition = null;
- }
- // If tryReplaceWithActualWord returns false, we don't know what word was
- // added to the user dictionary yet, so we keep the data and defer processing. The word will
- // be replaced when the user dictionary reports back with the actual word, which ends
- // up calling #onWordAddedToUserDictionary() in this class.
+ currentSettingsValues.mGestureInputEnabled,
+ 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)) {
+ mLastSelectionStart = textLength;
+ // We can't figure out the value of mLastSelectionEnd :(
+ // But at least if it's smaller than mLastSelectionStart something is wrong
+ if (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) {
@@ -851,12 +1022,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mKeyboardSwitcher.onFinishInputView();
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
if (mainKeyboardView != null) {
- mainKeyboardView.cancelAllMessages();
+ mainKeyboardView.cancelAllOngoingEvents();
+ mainKeyboardView.deallocateMemory();
}
// 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 */);
+ mRichImm.clearSubtypeCaches();
// Notify ResearchLogger
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart,
@@ -893,20 +1067,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- // TODO: refactor the following code to be less contrived.
- // "newSelStart != composingSpanEnd" || "newSelEnd != composingSpanEnd" means
- // that the cursor is not at the end of the composing span, or there is a selection.
- // "mLastSelectionStart != newSelStart" means that the cursor is not in the same place
- // as last time we were called (if there is a selection, it means the start hasn't
- // changed, so it's the end that did).
- final boolean selectionChanged = (newSelStart != composingSpanEnd
- || newSelEnd != composingSpanEnd) && mLastSelectionStart != newSelStart;
+ 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.
+ // 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.
@@ -928,7 +1099,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// state-related special processing to kick in.
mSpaceState = SPACE_STATE_NONE;
- if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) {
+ // 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
@@ -939,6 +1117,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// 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,
@@ -1050,17 +1235,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private void setSuggestionStripShownInternal(final boolean shown,
final boolean needsInputViewShown) {
// TODO: Modify this if we support suggestions with hard keyboard
- if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
+ if (onEvaluateInputViewShown() && mSuggestionStripView != null) {
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
final boolean inputViewShown = (mainKeyboardView != null)
? mainKeyboardView.isShown() : false;
final boolean shouldShowSuggestions = shown
&& (needsInputViewShown ? inputViewShown : true);
if (isFullscreenMode()) {
- mSuggestionsContainer.setVisibility(
+ mSuggestionStripView.setVisibility(
shouldShowSuggestions ? View.VISIBLE : View.GONE);
} else {
- mSuggestionsContainer.setVisibility(
+ mSuggestionStripView.setVisibility(
shouldShowSuggestions ? View.VISIBLE : View.INVISIBLE);
}
}
@@ -1076,12 +1261,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return currentHeight;
}
- final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
- if (mainKeyboardView == null) {
+ final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
+ if (visibleKeyboardView == null) {
return 0;
}
- final int keyboardHeight = mainKeyboardView.getHeight();
- final int suggestionsHeight = mSuggestionsContainer.getHeight();
+ // TODO: !!!!!!!!!!!!!!!!!!!! Handle different backing view heights between the main !!!
+ // keyboard and the emoji keyboard. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ final int keyboardHeight = visibleKeyboardView.getHeight();
+ final int suggestionsHeight = mSuggestionStripView.getHeight();
final int displayHeight = getResources().getDisplayMetrics().heightPixels;
final Rect rect = new Rect();
mKeyPreviewBackingView.getWindowVisibleDisplayFrame(rect);
@@ -1098,8 +1285,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onComputeInsets(final InputMethodService.Insets outInsets) {
super.onComputeInsets(outInsets);
- final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
- if (mainKeyboardView == null || mSuggestionsContainer == null) {
+ final View visibleKeyboardView = mKeyboardSwitcher.getVisibleKeyboardView();
+ if (visibleKeyboardView == null || mSuggestionStripView == null) {
return;
}
final int adjustedBackingHeight = getAdjustedBackingViewHeight();
@@ -1109,18 +1296,21 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// be considered.
// See {@link android.inputmethodservice.InputMethodService#onComputeInsets}.
final int extractHeight = isFullscreenMode() ? mExtractArea.getHeight() : 0;
- final int suggestionsHeight = (mSuggestionsContainer.getVisibility() == View.GONE) ? 0
- : mSuggestionsContainer.getHeight();
+ final int suggestionsHeight = (mSuggestionStripView.getVisibility() == View.GONE) ? 0
+ : mSuggestionStripView.getHeight();
final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
int visibleTopY = extraHeight;
// Need to set touchable region only if input view is being shown
- if (mainKeyboardView.isShown()) {
- if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
+ if (visibleKeyboardView.isShown()) {
+ // Note that the height of Emoji layout is the same as the height of the main keyboard
+ // and the suggestion strip
+ if (mKeyboardSwitcher.isShowingEmojiKeyboard()
+ || mSuggestionStripView.getVisibility() == View.VISIBLE) {
visibleTopY -= suggestionsHeight;
}
- final int touchY = mainKeyboardView.isShowingMoreKeysPanel() ? 0 : visibleTopY;
- final int touchWidth = mainKeyboardView.getWidth();
- final int touchHeight = mainKeyboardView.getHeight() + extraHeight
+ final int touchY = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY;
+ final int touchWidth = visibleKeyboardView.getWidth();
+ final int touchHeight = visibleKeyboardView.getHeight() + extraHeight
// Extend touchable region below the keyboard.
+ EXTENDED_TOUCHABLE_REGION_HEIGHT;
outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
@@ -1162,12 +1352,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private void resetEntireInputState(final int newCursorPosition) {
final boolean shouldFinishComposition = mWordComposer.isComposingWord();
resetComposingState(true /* alsoResetLastComposedWord */);
- if (mSettings.getCurrent().mBigramPredictionEnabled) {
+ final SettingsValues settingsValues = mSettings.getCurrent();
+ if (settingsValues.mBigramPredictionEnabled) {
clearSuggestionStrip();
} else {
- setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false);
+ setSuggestedWords(settingsValues.mSuggestPuncList, false);
}
- mConnection.resetCachesUponCursorMove(newCursorPosition, shouldFinishComposition);
+ mConnection.resetCachesUponCursorMoveAndReturnSuccess(newCursorPosition,
+ shouldFinishComposition);
}
private void resetComposingState(final boolean alsoResetLastComposedWord) {
@@ -1239,8 +1431,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
private boolean maybeDoubleSpacePeriod() {
- if (!mSettings.getCurrent().mCorrectionEnabled) return false;
- if (!mSettings.getCurrent().mUseDoubleSpacePeriod) return false;
+ final SettingsValues settingsValues = mSettings.getCurrent();
+ if (!settingsValues.mCorrectionEnabled) return false;
+ if (!settingsValues.mUseDoubleSpacePeriod) return false;
if (!mHandler.isAcceptingDoubleSpacePeriod()) return false;
final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0);
if (lastThree != null && lastThree.length() == 3
@@ -1270,7 +1463,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|| 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_CLOSING_ANGLE_BRACKET
+ || codePoint == Constants.CODE_PLUS;
}
// Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
@@ -1279,7 +1473,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
public void addWordToUserDictionary(final String word) {
if (TextUtils.isEmpty(word)) {
// Probably never supposed to happen, but just in case.
- mPositionalInfoForUserDictPendingAddition = null;
return;
}
final String wordToEdit;
@@ -1291,39 +1484,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mUserDictionary.addWordToUserDictionary(wordToEdit);
}
- public void onWordAddedToUserDictionary(final String newSpelling) {
- // If word was added but not by us, bail out
- if (null == mPositionalInfoForUserDictPendingAddition) return;
- if (mWordComposer.isComposingWord()) {
- // We are late... give up and return
- mPositionalInfoForUserDictPendingAddition = null;
- return;
- }
- mPositionalInfoForUserDictPendingAddition.setActualWordBeingAdded(newSpelling);
- if (mPositionalInfoForUserDictPendingAddition.tryReplaceWithActualWord(
- mConnection, getCurrentInputEditorInfo(), mLastSelectionEnd,
- mSubtypeSwitcher.getCurrentSubtypeLocale())) {
- mPositionalInfoForUserDictPendingAddition = null;
- }
- }
-
- private static boolean isAlphabet(final int code) {
- return Character.isLetter(code);
- }
-
private void onSettingsKeyPressed() {
if (isShowingOptionDialog()) return;
showSubtypeSelectorAndSettings();
}
- // Virtual codes representing custom requests. These are used in onCustomRequest() below.
- public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1;
-
@Override
public boolean onCustomRequest(final int requestCode) {
if (isShowingOptionDialog()) return false;
switch (requestCode) {
- case CODE_SHOW_INPUT_METHOD_PICKER:
+ case Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER:
if (mRichImm.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
mRichImm.getInputMethodManager().showInputMethodPicker();
return true;
@@ -1421,7 +1591,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
break;
case Constants.CODE_SHIFT:
// Note: Calling back to the keyboard on Shift key is handled in
- // {@link #onPressKey(int,boolean)} and {@link #onReleaseKey(int,boolean)}.
+ // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
final Keyboard currentKeyboard = switcher.getKeyboard();
if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
// TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
@@ -1435,7 +1605,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
break;
case Constants.CODE_SWITCH_ALPHA_SYMBOL:
// Note: Calling back to the keyboard on symbol key is handled in
- // {@link #onPressKey(int,boolean)} and {@link #onReleaseKey(int,boolean)}.
+ // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
break;
case Constants.CODE_SETTINGS:
onSettingsKeyPressed();
@@ -1452,10 +1622,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
case Constants.CODE_LANGUAGE_SWITCH:
handleLanguageSwitchKey();
break;
- case Constants.CODE_RESEARCH:
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.getInstance().onResearchKeySelected(this);
- }
+ 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();
@@ -1503,14 +1672,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final int spaceState) {
mSpaceState = SPACE_STATE_NONE;
final boolean didAutoCorrect;
- if (mSettings.getCurrent().isWordSeparator(primaryCode)) {
+ final SettingsValues settingsValues = mSettings.getCurrent();
+ if (settingsValues.isWordSeparator(primaryCode)) {
didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
} else {
didAutoCorrect = false;
if (SPACE_STATE_PHANTOM == spaceState) {
- if (mSettings.isInternal()) {
+ if (settingsValues.mIsInternal) {
if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
- Stats.onAutoCorrection(
+ LatinImeLoggerUtils.onAutoCorrection(
"", mWordComposer.getTypedWord(), " ", mWordComposer);
}
}
@@ -1547,6 +1717,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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();
@@ -1565,18 +1740,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onStartBatchInput() {
- BatchInputUpdater.getInstance().onStartBatchInput(this);
+ mInputUpdater.onStartBatchInput();
mHandler.cancelUpdateSuggestionStrip();
mConnection.beginBatchEdit();
+ final SettingsValues settingsValues = mSettings.getCurrent();
if (mWordComposer.isComposingWord()) {
- if (mSettings.isInternal()) {
+ if (settingsValues.mIsInternal) {
if (mWordComposer.isBatchMode()) {
- Stats.onAutoCorrection("", mWordComposer.getTypedWord(), " ", mWordComposer);
+ LatinImeLoggerUtils.onAutoCorrection(
+ "", mWordComposer.getTypedWord(), " ", mWordComposer);
}
}
final int wordComposerSize = mWordComposer.size();
// Since isComposingWord() is true, the size is at least 1.
- final int lastChar = mWordComposer.getCodeBeforeCursor();
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
// If we are in the middle of a recorrection, we need to commit the recorrection
// first so that we can insert the batch input at the current cursor position.
@@ -1597,53 +1773,48 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
if (Character.isLetterOrDigit(codePointBeforeCursor)
- || mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) {
+ || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
mSpaceState = SPACE_STATE_PHANTOM;
}
mConnection.endBatchEdit();
mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
}
- private static final class BatchInputUpdater implements Handler.Callback {
+ private static final class InputUpdater implements Handler.Callback {
private final Handler mHandler;
- private LatinIME mLatinIme;
+ private final LatinIME mLatinIme;
private final Object mLock = new Object();
private boolean mInBatchInput; // synchronized using {@link #mLock}.
- private BatchInputUpdater() {
+ private InputUpdater(final LatinIME latinIme) {
final HandlerThread handlerThread = new HandlerThread(
- BatchInputUpdater.class.getSimpleName());
+ InputUpdater.class.getSimpleName());
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper(), this);
- }
-
- // Initialization-on-demand holder
- private static final class OnDemandInitializationHolder {
- public static final BatchInputUpdater sInstance = new BatchInputUpdater();
- }
-
- public static BatchInputUpdater getInstance() {
- return OnDemandInitializationHolder.sInstance;
+ 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) {
switch (msg.what) {
- case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
- updateBatchInput((InputPointers)msg.obj);
- break;
+ case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
+ updateBatchInput((InputPointers)msg.obj);
+ break;
+ case MSG_GET_SUGGESTED_WORDS:
+ mLatinIme.getSuggestedWords(msg.arg1, (OnGetSuggestedWordsCallback) msg.obj);
+ break;
}
return true;
}
// Run in the UI thread.
- public void onStartBatchInput(final LatinIME latinIme) {
+ public void onStartBatchInput() {
synchronized (mLock) {
mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
mInBatchInput = true;
- mLatinIme = latinIme;
mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */);
}
@@ -1656,9 +1827,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Batch input has ended or canceled while the message was being delivered.
return;
}
- final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
- mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
- suggestedWords, false /* dismissGestureFloatingPreviewText */);
+
+ getSuggestedWordsGestureLocked(batchPointers, new OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
+ mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+ suggestedWords, false /* dismissGestureFloatingPreviewText */);
+ }
+ });
}
}
@@ -1681,35 +1857,57 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
// Run in the UI thread.
- public SuggestedWords onEndBatchInput(final InputPointers batchPointers) {
- synchronized (mLock) {
- mInBatchInput = false;
- final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
- mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
- suggestedWords, true /* dismissGestureFloatingPreviewText */);
- return suggestedWords;
+ public void onEndBatchInput(final InputPointers batchPointers) {
+ synchronized(mLock) {
+ getSuggestedWordsGestureLocked(batchPointers, 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 SuggestedWords getSuggestedWordsGestureLocked(final InputPointers batchPointers) {
+ private void getSuggestedWordsGestureLocked(final InputPointers batchPointers,
+ final OnGetSuggestedWordsCallback callback) {
mLatinIme.mWordComposer.setBatchInputPointers(batchPointers);
- final SuggestedWords suggestedWords =
- mLatinIme.getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_GESTURE);
- final int suggestionCount = suggestedWords.size();
- if (suggestionCount <= 1) {
- final String mostProbableSuggestion = (suggestionCount == 0) ? null
- : suggestedWords.getWord(0);
- return mLatinIme.getOlderSuggestions(mostProbableSuggestion);
- }
- return suggestedWords;
+ mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE,
+ 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);
+ }
+ });
+ }
+
+ public void getSuggestedWords(final int sessionId,
+ final OnGetSuggestedWordsCallback callback) {
+ mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, 0, callback).sendToTarget();
+ }
+
+ private void onDestroy() {
+ mHandler.removeMessages(MSG_GET_SUGGESTED_WORDS);
+ mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
+ mHandler.getLooper().quit();
}
}
+ // This method must run in UI Thread.
private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
final boolean dismissGestureFloatingPreviewText) {
- showSuggestionStrip(suggestedWords, null);
+ showSuggestionStrip(suggestedWords);
final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
mainKeyboardView.showGestureFloatingPreviewText(suggestedWords);
if (dismissGestureFloatingPreviewText) {
@@ -1719,24 +1917,48 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onUpdateBatchInput(final InputPointers batchPointers) {
- BatchInputUpdater.getInstance().onUpdateBatchInput(batchPointers);
+ if (mSettings.getCurrent().mPhraseGestureEnabled) {
+ final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
+ if (null != candidate) {
+ 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());
+ }
+ }
+ }
+ mInputUpdater.onUpdateBatchInput(batchPointers);
}
- @Override
- public void onEndBatchInput(final InputPointers batchPointers) {
- final SuggestedWords suggestedWords = BatchInputUpdater.getInstance().onEndBatchInput(
- batchPointers);
+ // 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;
}
- mWordComposer.setBatchInputWord(batchInputText);
mConnection.beginBatchEdit();
if (SPACE_STATE_PHANTOM == mSpaceState) {
promotePhantomSpace();
}
- mConnection.setComposingText(batchInputText, 1);
+ 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) {
@@ -1747,6 +1969,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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))) {
@@ -1782,7 +2009,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
@Override
public void onCancelBatchInput() {
- BatchInputUpdater.getInstance().onCancelBatchInput();
+ mInputUpdater.onCancelBatchInput();
}
private void handleBackspace(final int spaceState) {
@@ -1798,28 +2025,29 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// When we exit this if-clause, mWordComposer.isComposingWord() will return false.
}
if (mWordComposer.isComposingWord()) {
- final int length = mWordComposer.size();
- if (length > 0) {
- 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();
+ if (mWordComposer.isBatchMode()) {
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ final String word = mWordComposer.getTypedWord();
+ ResearchLogger.latinIME_handleBackspace_batch(word, 1);
}
- mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
- mHandler.postUpdateSuggestionStrip();
+ final String rejectedSuggestion = mWordComposer.getTypedWord();
+ mWordComposer.reset();
+ mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
} else {
- mConnection.deleteSurroundingText(1, 0);
+ 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 (mSettings.isInternal()) {
- Stats.onAutoCorrectionCancellation();
+ if (currentSettings.mIsInternal) {
+ LatinImeLoggerUtils.onAutoCorrectionCancellation();
}
revertCommit();
return;
@@ -1828,8 +2056,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// 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.
- final int length = mEnteredText.length();
- mConnection.deleteSurroundingText(length, 0);
+ mConnection.deleteSurroundingText(mEnteredText.length(), 0);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
}
@@ -1875,6 +2102,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// This should never happen.
Log.e(TAG, "Backspace when we don't know the selection position");
}
+ final int lengthToDelete = Character.isSupplementaryCodePoint(
+ mConnection.getCodePointBeforeCursor()) ? 2 : 1;
if (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
@@ -1882,22 +2111,28 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// relying on this behavior so we continue to support it for older apps.
sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL);
} else {
- mConnection.deleteSurroundingText(1, 0);
+ mConnection.deleteSurroundingText(lengthToDelete, 0);
}
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_handleBackspace(1, true /* shouldUncommitLogUnit */);
+ ResearchLogger.latinIME_handleBackspace(lengthToDelete,
+ true /* shouldUncommitLogUnit */);
}
if (mDeleteCount > DELETE_ACCELERATE_AT) {
- mConnection.deleteSurroundingText(1, 0);
+ final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
+ mConnection.getCodePointBeforeCursor()) ? 2 : 1;
+ mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.latinIME_handleBackspace(1,
+ ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain,
true /* shouldUncommitLogUnit */);
}
}
}
- if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) {
+ if (currentSettings.isSuggestionsRequested(mDisplayOrientation)
+ && currentSettings.mCurrentLanguageHasSpaces) {
restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
}
+ // We just removed a character. We need to update the auto-caps state.
+ mKeyboardSwitcher.updateShiftState();
}
}
@@ -1912,8 +2147,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
&& isFromSuggestionStrip) {
- if (mSettings.getCurrent().isUsuallyPrecededBySpace(code)) return false;
- if (mSettings.getCurrent().isUsuallyFollowedBySpace(code)) return true;
+ final SettingsValues currentSettings = mSettings.getCurrent();
+ if (currentSettings.isUsuallyPrecededBySpace(code)) return false;
+ if (currentSettings.isUsuallyFollowedBySpace(code)) return true;
mConnection.removeTrailingSpace();
}
return false;
@@ -1921,12 +2157,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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.
- if (SPACE_STATE_PHANTOM == spaceState &&
- !mSettings.getCurrent().isWordConnector(primaryCode)) {
+ 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");
@@ -1940,18 +2179,26 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
resetEntireInputState(mLastSelectionStart);
isComposingWord = false;
}
- // NOTE: isCursorTouchingWord() is a blocking IPC call, so it often takes several
- // dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
- // thread here.
- if (!isComposingWord && (isAlphabet(primaryCode)
- || mSettings.getCurrent().isWordConnector(primaryCode))
- && mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation) &&
- !mConnection.isCursorTouchingWord(mSettings.getCurrent())) {
+ // 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. The idea here is, single quote is not a
- // separator and it should be treated as a normal character, except in the first
- // position where it should not start composing a word.
- isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode);
+ // 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
@@ -1989,8 +2236,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint();
}
mHandler.postUpdateSuggestionStrip();
- if (mSettings.isInternal()) {
- Utils.Stats.onNonSeparator((char)primaryCode, x, y);
+ if (currentSettings.mIsInternal) {
+ LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y);
}
}
@@ -2002,9 +2249,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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(), mSettings.getCurrentLocale(),
- mSettings.getWordSeparators());
+ 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
@@ -2029,19 +2277,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mKeyboardSwitcher.updateShiftState();
}
- // Returns true if we did an autocorrection, false otherwise.
+ // 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()) {
- if (mSettings.getCurrent().mCorrectionEnabled) {
- // TODO: maybe cache Strings in an <String> sparse array or something
- commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1));
+ if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing
+ if (currentSettings.mCorrectionEnabled) {
+ final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
+ : new String(new int[] { primaryCode }, 0, 1);
+ commitCurrentAutoCorrection(separator);
didAutoCorrect = true;
} else {
commitTyped(new String(new int[]{primaryCode}, 0, 1));
@@ -2052,16 +2305,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
Constants.SUGGESTION_STRIP_COORDINATE == x);
if (SPACE_STATE_PHANTOM == spaceState &&
- mSettings.getCurrent().isUsuallyPrecededBySpace(primaryCode)) {
+ currentSettings.isUsuallyPrecededBySpace(primaryCode)) {
promotePhantomSpace();
}
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
}
- sendKeyCodePoint(primaryCode);
+
+ if (!shouldAvoidSendingCode) {
+ sendKeyCodePoint(primaryCode);
+ }
if (Constants.CODE_SPACE == primaryCode) {
- if (mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) {
+ if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
if (maybeDoubleSpacePeriod()) {
mSpaceState = SPACE_STATE_DOUBLE;
} else if (!isShowingPunctuationList()) {
@@ -2076,7 +2332,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
swapSwapperAndSpace();
mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
} else if (SPACE_STATE_PHANTOM == spaceState
- && mSettings.getCurrent().isUsuallyFollowedBySpace(primaryCode)) {
+ && 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
@@ -2094,8 +2350,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// already displayed or not, so it's okay.
setPunctuationSuggestions();
}
- if (mSettings.isInternal()) {
- Utils.Stats.onSeparator((char)primaryCode, x, y);
+ if (currentSettings.mIsInternal) {
+ LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y);
}
mKeyboardSwitcher.updateShiftState();
@@ -2127,17 +2383,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
private boolean isSuggestionsStripVisible() {
+ final SettingsValues currentSettings = mSettings.getCurrent();
if (mSuggestionStripView == null)
return false;
if (mSuggestionStripView.isShowingAddToDictionaryHint())
return true;
- if (null == mSettings.getCurrent())
+ if (null == currentSettings)
return false;
- if (!mSettings.getCurrent().isSuggestionStripVisibleInOrientation(mDisplayOrientation))
+ if (!currentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
return false;
- if (mSettings.getCurrent().isApplicationSpecifiedCompletionsOn())
+ if (currentSettings.isApplicationSpecifiedCompletionsOn())
return true;
- return mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation);
+ return currentSettings.isSuggestionsRequested(mDisplayOrientation);
}
private void clearSuggestionStrip() {
@@ -2170,10 +2427,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private void updateSuggestionStrip() {
mHandler.cancelUpdateSuggestionStrip();
+ final SettingsValues currentSettings = mSettings.getCurrent();
// Check if we have a suggestion engine attached.
if (mSuggest == null
- || !mSettings.getCurrent().isSuggestionsRequested(mDisplayOrientation)) {
+ || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
if (mWordComposer.isComposingWord()) {
Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
+ "requested!");
@@ -2181,37 +2439,65 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return;
}
- if (!mWordComposer.isComposingWord() && !mSettings.getCurrent().mBigramPredictionEnabled) {
+ if (!mWordComposer.isComposingWord() && !currentSettings.mBigramPredictionEnabled) {
setPunctuationSuggestions();
return;
}
- final SuggestedWords suggestedWords =
- getSuggestedWordsOrOlderSuggestions(Suggest.SESSION_TYPING);
- final String typedWord = mWordComposer.getTypedWord();
- showSuggestionStrip(suggestedWords, typedWord);
+ final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>();
+ getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING,
+ 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);
+ }
}
- private SuggestedWords getSuggestedWords(final int sessionId) {
+ private void getSuggestedWords(final int sessionId,
+ final OnGetSuggestedWordsCallback callback) {
final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
- if (keyboard == null || mSuggest == null) {
- return SuggestedWords.EMPTY;
+ final Suggest suggest = mSuggest;
+ if (keyboard == null || suggest == null) {
+ callback.onGetSuggestedWords(SuggestedWords.EMPTY);
+ return;
}
// Get the word on which we should search the bigrams. If we are composing a word, it's
// whatever is *before* the half-committed word in the buffer, hence 2; if we aren't, we
// should just skip whitespace if any, so 1.
- // TODO: this is slow (2-way IPC) - we should probably cache this instead.
- final String prevWord =
- mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators,
- mWordComposer.isComposingWord() ? 2 : 1);
- return mSuggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
- mSettings.getBlockPotentiallyOffensive(),
- mSettings.getCurrent().mCorrectionEnabled, sessionId);
+ 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, callback);
}
- private SuggestedWords getSuggestedWordsOrOlderSuggestions(final int sessionId) {
- return maybeRetrieveOlderSuggestions(mWordComposer.getTypedWord(),
- getSuggestedWords(sessionId));
+ private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId,
+ final OnGetSuggestedWordsCallback callback) {
+ mInputUpdater.getSuggestedWords(sessionId, new OnGetSuggestedWordsCallback() {
+ @Override
+ public void onGetSuggestedWords(SuggestedWords suggestedWords) {
+ callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions(
+ mWordComposer.getTypedWord(), suggestedWords));
+ }
+ });
}
private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
@@ -2251,25 +2537,42 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
false /* isPrediction */);
}
- private void showSuggestionStrip(final SuggestedWords suggestedWords, final String typedWord) {
- if (suggestedWords.isEmpty()) {
- clearSuggestionStrip();
- return;
- }
+ private void setAutoCorrection(final SuggestedWords suggestedWords, final String typedWord) {
+ if (suggestedWords.isEmpty()) return;
final String autoCorrection;
if (suggestedWords.mWillAutoCorrect) {
- autoCorrection = suggestedWords.getWord(1);
+ 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);
- final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
- setSuggestedWords(suggestedWords, isAutoCorrection);
- setAutoCorrectionIndicator(isAutoCorrection);
- setSuggestionStripShown(isSuggestionsStripVisible());
}
- private void commitCurrentAutoCorrection(final String separatorString) {
+ private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
+ final String typedWord) {
+ if (suggestedWords.isEmpty()) {
+ clearSuggestionStrip();
+ return;
+ }
+ setAutoCorrection(suggestedWords, typedWord);
+ final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
+ setSuggestedWords(suggestedWords, isAutoCorrection);
+ setAutoCorrectionIndicator(isAutoCorrection);
+ setSuggestionStripShown(isSuggestionsStripVisible());
+ }
+
+ private void showSuggestionStrip(final SuggestedWords suggestedWords) {
+ if (suggestedWords.isEmpty()) {
+ clearSuggestionStrip();
+ return;
+ }
+ 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();
@@ -2284,16 +2587,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
+ "is empty? Impossible! I must commit suicide.");
}
if (mSettings.isInternal()) {
- Stats.onAutoCorrection(typedWord, autoCorrection, separatorString, mWordComposer);
+ LatinImeLoggerUtils.onAutoCorrection(
+ typedWord, autoCorrection, separator, mWordComposer);
}
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
final SuggestedWords suggestedWords = mSuggestedWords;
ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
- separatorString, mWordComposer.isBatchMode(), suggestedWords);
+ separator, mWordComposer.isBatchMode(), suggestedWords);
}
mExpectingUpdateSelection = true;
commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
- separatorString);
+ 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
@@ -2331,18 +2635,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
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 (!mSettings.getCurrent().isWordSeparator(firstChar)
- || mSettings.getCurrent().isUsuallyPrecededBySpace(firstChar)) {
+ if (!currentSettings.isWordSeparator(firstChar)
+ || currentSettings.isUsuallyPrecededBySpace(firstChar)) {
promotePhantomSpace();
}
}
- if (mSettings.getCurrent().isApplicationSpecifiedCompletionsOn()
+ if (currentSettings.isApplicationSpecifiedCompletionsOn()
&& mApplicationSpecifiedCompletions != null
&& index >= 0 && index < mApplicationSpecifiedCompletions.length) {
mSuggestedWords = SuggestedWords.EMPTY;
@@ -2366,7 +2671,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
LastComposedWord.NOT_A_SEPARATOR);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion,
- mWordComposer.isBatchMode());
+ mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind,
+ suggestionInfo.mSourceDict.mDictType);
}
mConnection.endBatchEdit();
// Don't allow cancellation of manual pick
@@ -2379,20 +2685,21 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// 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)
- && mSuggest != null
+ && suggest != null
// If the suggestion is not in the dictionary, the hint should be shown.
- && !AutoCorrection.isValidWord(mSuggest, suggestion, true);
+ && !AutoCorrectionUtils.isValidWord(suggest, suggestion, true);
- if (mSettings.isInternal()) {
- Stats.onSeparator((char)Constants.CODE_SPACE,
+ if (currentSettings.mIsInternal) {
+ LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE,
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
}
if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
mSuggestionStripView.showAddToDictionaryHint(
- suggestion, mSettings.getCurrent().mHintToSaveText);
+ suggestion, currentSettings.mHintToSaveText);
} else {
// If we're not showing the "Touch again to save", then update the suggestion strip.
mHandler.postUpdateSuggestionStrip();
@@ -2418,10 +2725,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
private void setPunctuationSuggestions() {
- if (mSettings.getCurrent().mBigramPredictionEnabled) {
+ final SettingsValues currentSettings = mSettings.getCurrent();
+ if (currentSettings.mBigramPredictionEnabled) {
clearSuggestionStrip();
} else {
- setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false);
+ setSuggestedWords(currentSettings.mSuggestPuncList, false);
}
setAutoCorrectionIndicator(false);
setSuggestionStripShown(isSuggestionsStripVisible());
@@ -2429,21 +2737,20 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private String addToUserHistoryDictionary(final String suggestion) {
if (TextUtils.isEmpty(suggestion)) return null;
- if (mSuggest == null) 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.
- if (!mSettings.getCurrent().mCorrectionEnabled) return null;
+ final SettingsValues currentSettings = mSettings.getCurrent();
+ if (!currentSettings.mCorrectionEnabled) return null;
- final Suggest suggest = mSuggest;
- final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
- if (suggest == null || userHistoryDictionary == null) {
- // Avoid concurrent issue
- return null;
- }
- final String prevWord
- = mConnection.getNthPreviousWord(mSettings.getCurrent().mWordSeparators, 2);
+ final UserHistoryPredictionDictionary userHistoryPredictionDictionary =
+ mUserHistoryPredictionDictionary;
+ if (userHistoryPredictionDictionary == null) return null;
+
+ final String prevWord = mConnection.getNthPreviousWord(currentSettings.mWordSeparators, 2);
final String secondWord;
if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
@@ -2452,13 +2759,21 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
// 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 = AutoCorrection.getMaxFrequency(
+ final int maxFreq = AutoCorrectionUtils.getMaxFrequency(
suggest.getUnigramDictionaries(), suggestion);
if (maxFreq == 0) return null;
- userHistoryDictionary.addToUserHistory(prevWord, secondWord, maxFreq > 0);
+ userHistoryPredictionDictionary
+ .addToPersonalizationPredictionDictionary(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.
@@ -2468,74 +2783,98 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// 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;
- if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) return;
- final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(),
+ 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.
- if (range.mCharsBefore > mLastSelectionStart) return;
+ final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor();
+ if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return;
final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
final String typedWord = range.mWord.toString();
- if (range.mWord instanceof SpannableString) {
- final SpannableString spannableString = (SpannableString)range.mWord;
- int i = 0;
- for (Object object : spannableString.getSpans(0, spannableString.length(),
- SuggestionSpan.class)) {
- SuggestionSpan span = (SuggestionSpan)object;
- for (String s : span.getSuggestions()) {
- ++i;
- if (!TextUtils.equals(s, typedWord)) {
- suggestions.add(new SuggestedWordInfo(s,
- SuggestionStripView.MAX_SUGGESTIONS - i,
- SuggestedWordInfo.KIND_RESUMED, Dictionary.TYPE_RESUMED));
- }
+ 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(range.mCharsBefore);
- mConnection.setComposingRegion(mLastSelectionStart - range.mCharsBefore,
- mLastSelectionEnd + range.mCharsAfter);
- final SuggestedWords suggestedWords;
+ 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.
- final SuggestedWords suggestedWordsIncludingTypedWord =
- getSuggestedWords(Suggest.SESSION_TYPING);
- if (suggestedWordsIncludingTypedWord.size() > 1) {
- // We were able to compute new suggestions for this word.
- // Remove the typed word, since we don't want to display it in this case.
- // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to false.
- suggestedWords =
- suggestedWordsIncludingTypedWord.getSuggestedWordsExcludingTypedWord();
- } else {
- // No saved suggestions, and we were unable to compute any good one either.
- // Rather than displaying an empty suggestion strip, we'll display the original
- // word alone in the middle.
- // Since there is only one word, willAutoCorrect is false.
- suggestedWords = suggestedWordsIncludingTypedWord;
- }
+ mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING,
+ 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.
- suggestedWords = new SuggestedWords(suggestions,
+ 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 where suggestedWords is affected. We also need to reset
- // mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching the text to adapt it.
- // TODO: remove mIsAutoCorrectionIndicator on (see comment on definition)
+ // 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;
- showSuggestionStrip(suggestedWords, typedWord);
+ mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord);
}
/**
@@ -2565,6 +2904,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
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;
+ }
+ tryFixLyingCursorPosition();
+ if (tryResumeSuggestions) mHandler.postResumeSuggestions();
+ }
+
private void revertCommit() {
final String previousWord = mLastComposedWord.mPrevWord;
final String originallyTypedWord = mLastComposedWord.mTypedWord;
@@ -2589,11 +2949,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
mConnection.deleteSurroundingText(deleteLength, 0);
if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
- mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
+ mUserHistoryPredictionDictionary.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);
}
- mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1);
if (mSettings.isInternal()) {
- Stats.onSeparator(mLastComposedWord.mSeparatorString,
+ LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
}
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -2609,7 +2980,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// This essentially inserts a space, and that's it.
public void promotePhantomSpace() {
- if (mSettings.getCurrent().shouldInsertSpacesAutomatically()
+ final SettingsValues currentSettings = mSettings.getCurrent();
+ if (currentSettings.shouldInsertSpacesAutomatically()
+ && currentSettings.mCurrentLanguageHasSpaces
&& !mConnection.textBeforeCursorLooksLikeURL()) {
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.latinIME_promotePhantomSpace();
@@ -2618,38 +2991,60 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- // Used by the RingCharBuffer
- public boolean isWordSeparator(final int code) {
- return mSettings.getCurrent().isWordSeparator(code);
- }
-
// TODO: Make this private
// Outside LatinIME, only used by the {@link InputTestsBase} test suite.
@UsedForTesting
void loadKeyboard() {
- // TODO: Why are we calling {@link #loadSettings()} and {@link #initSuggest()} in a
- // different order than in {@link #onStartInputView}?
- initSuggest();
+ // Since we are switching languages, the most urgent thing is to let the keyboard graphics
+ // update. LoadKeyboard does that, but we need to wait for buffer flip for it to be on
+ // the screen. Anything we do right now will delay this, so wait until the next frame
+ // before we do the rest, like reopening dictionaries and updating suggestions. So we
+ // post a message.
+ mHandler.postReopenDictionaries();
loadSettings();
if (mKeyboardSwitcher.getMainKeyboardView() != null) {
// Reload keyboard because the current language has been changed.
mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettings.getCurrent());
}
- // Since we just changed languages, we should re-evaluate suggestions with whatever word
- // we are currently composing. If we are not composing anything, we may want to display
- // predictions or punctuation signs (which is done by the updateSuggestionStrip anyway).
- mHandler.postUpdateSuggestionStrip();
}
- // Callback called by PointerTracker through the KeyboardActionListener. This is called when a
- // key is depressed; release matching call is onReleaseKey below.
+ 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.
+ return;
+ }
+ if (repeatCount > 0) {
+ if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) {
+ // No need to feedback when repeat delete key will have no effect.
+ return;
+ }
+ // TODO: Use event time that the last feedback has been generated instead of relying on
+ // a repeat count to thin out feedback.
+ if (repeatCount % PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT == 0) {
+ return;
+ }
+ }
+ final AudioAndHapticFeedbackManager feedbackManager =
+ AudioAndHapticFeedbackManager.getInstance();
+ if (repeatCount == 0) {
+ // TODO: Reconsider how to perform haptic feedback when repeating key.
+ feedbackManager.performHapticFeedback(keyboardView);
+ }
+ feedbackManager.performAudioFeedback(code);
+ }
+
+ // Callback of the {@link KeyboardActionListener}. This is called when a key is depressed;
+ // release matching call is {@link #onReleaseKey(int,boolean)} below.
@Override
- public void onPressKey(final int primaryCode, final boolean isSinglePointer) {
+ public void onPressKey(final int primaryCode, final int repeatCount,
+ final boolean isSinglePointer) {
mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer);
+ hapticAndAudioFeedback(primaryCode, repeatCount);
}
- // Callback by PointerTracker through the KeyboardActionListener. This is called when a key
- // is released; press matching call is onPressKey above.
+ // Callback of the {@link KeyboardActionListener}. This is called when a key is released;
+ // press matching call is {@link #onPressKey(int,int,boolean)} above.
@Override
public void onReleaseKey(final int primaryCode, final boolean withSliding) {
mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
@@ -2665,17 +3060,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
break;
}
}
-
- if (Constants.CODE_DELETE == primaryCode) {
- // This is a stopgap solution to avoid leaving a high surrogate alone in a text view.
- // In the future, we need to deprecate deteleSurroundingText() and have a surrogate
- // pair-friendly way of deleting characters in InputConnection.
- // TODO: use getCodePointBeforeCursor instead to improve performance
- final CharSequence lastChar = mConnection.getTextBeforeCursor(1, 0);
- if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
- mConnection.deleteSurroundingText(1, 0);
- }
- }
}
// Hooks for hardware keyboard
@@ -2746,7 +3130,7 @@ 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(Utils.getAcitivityTitleResId(this, SettingsActivity.class)),
+ getString(ApplicationUtils.getAcitivityTitleResId(this, SettingsActivity.class)),
};
final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
@Override
@@ -2799,6 +3183,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : 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();
+ }
+
+ // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
+ @UsedForTesting
+ /* package for test */ boolean hasMainDictionary() {
+ return mSuggest.hasMainDictionary();
+ }
+
+ // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
+ @UsedForTesting
+ /* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
+ mSuggest.resetMainDict(this, locale, null);
+ }
+
public void debugDumpStateAndCrashWithException(final String context) {
final StringBuilder s = new StringBuilder(mAppWorkAroundsUtils.toString());
s.append("\nAttributes : ").append(mSettings.getCurrent().mInputAttributes)
diff --git a/java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java b/java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java
deleted file mode 100644
index a8800007a..000000000
--- a/java/src/com/android/inputmethod/latin/PositionalInfoForUserDictPendingAddition.java
+++ /dev/null
@@ -1,106 +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;
-
-import android.view.inputmethod.EditorInfo;
-
-import java.util.Locale;
-
-/**
- * Holder class for data about a word already committed but that may still be edited.
- *
- * When the user chooses to add a word to the user dictionary by pressing the appropriate
- * suggestion, a dialog is presented to give a chance to edit the word before it is actually
- * registered as a user dictionary word. If the word is actually modified, the IME needs to
- * go back and replace the word that was committed with the amended version.
- * The word we need to replace with will only be known after it's actually committed, so
- * the IME needs to take a note of what it has to replace and where it is.
- * This class encapsulates this data.
- */
-public final class PositionalInfoForUserDictPendingAddition {
- final private String mOriginalWord;
- final private int mCursorPos; // Position of the cursor after the word
- final private EditorInfo mEditorInfo; // On what binding this has been added
- final private int mCapitalizedMode;
- private String mActualWordBeingAdded;
-
- public PositionalInfoForUserDictPendingAddition(final String word, final int cursorPos,
- final EditorInfo editorInfo, final int capitalizedMode) {
- mOriginalWord = word;
- mCursorPos = cursorPos;
- mEditorInfo = editorInfo;
- mCapitalizedMode = capitalizedMode;
- }
-
- public void setActualWordBeingAdded(final String actualWordBeingAdded) {
- mActualWordBeingAdded = actualWordBeingAdded;
- }
-
- /**
- * Try to replace the string at the remembered position with the actual word being added.
- *
- * After the user validated the word being added, the IME has to replace the old version
- * (which has been committed in the text view) with the amended version if it's different.
- * This method tries to do that, but may fail because the IME is not yet ready to do so -
- * for example, it is still waiting for the new string, or it is waiting to return to the text
- * view in which the amendment should be made. In these cases, we should keep the data
- * and wait until all conditions are met.
- * This method returns true if the replacement has been successfully made and this data
- * can be forgotten; it returns false if the replacement can't be made yet and we need to
- * keep this until a later time.
- * The IME knows about the actual word being added through a callback called by the
- * user dictionary facility of the device. When this callback comes, the keyboard may still
- * be connected to the edition dialog, or it may have already returned to the original text
- * field. Replacement has to work in both cases.
- * Accordingly, this method is called at two different points in time : upon getting the
- * event that a new word was added to the user dictionary, and upon starting up in a
- * new text field.
- * @param connection The RichInputConnection through which to contact the editor.
- * @param editorInfo Information pertaining to the editor we are currently in.
- * @param currentCursorPosition The current cursor position, for checking purposes.
- * @param locale The locale for changing case, if necessary
- * @return true if the edit has been successfully made, false if we need to try again later
- */
- public boolean tryReplaceWithActualWord(final RichInputConnection connection,
- final EditorInfo editorInfo, final int currentCursorPosition, final Locale locale) {
- // If we still don't know the actual word being added, we need to try again later.
- if (null == mActualWordBeingAdded) return false;
- // The entered text and the registered text were the same anyway : we can
- // return success right away even if focus has not returned yet to the text field we
- // want to amend.
- if (mActualWordBeingAdded.equals(mOriginalWord)) return true;
- // Not the same text field : we need to try again later. This happens when the addition
- // is reported by the user dictionary provider before the focus has moved back to the
- // original text view, so the IME is still in the text view of the dialog and has no way to
- // edit the original text view at this time.
- if (!mEditorInfo.packageName.equals(editorInfo.packageName)
- || mEditorInfo.fieldId != editorInfo.fieldId) {
- return false;
- }
- // Same text field, but not the same cursor position : we give up, so we return success
- // so that it won't be tried again
- if (currentCursorPosition != mCursorPos) return true;
- // We have made all the checks : do the replacement and report success
- // If this was auto-capitalized, we need to restore the case before committing
- final String wordWithCaseFixed = CapsModeUtils.applyAutoCapsMode(mActualWordBeingAdded,
- mCapitalizedMode, locale);
- connection.setComposingRegion(currentCursorPosition - mOriginalWord.length(),
- currentCursorPosition);
- connection.commitText(wordWithCaseFixed, wordWithCaseFixed.length());
- return true;
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 980215de6..8580a6e54 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -17,7 +17,6 @@
package com.android.inputmethod.latin;
import android.inputmethodservice.InputMethodService;
-import android.text.SpannableString;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
@@ -28,6 +27,12 @@ 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.utils.CapsModeUtils;
+import com.android.inputmethod.latin.utils.DebugLogUtils;
+import com.android.inputmethod.latin.utils.SpannableStringUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.latin.utils.TextRange;
import com.android.inputmethod.research.ResearchLogger;
import java.util.Locale;
@@ -47,16 +52,19 @@ public final class RichInputConnection {
private static final boolean DEBUG_PREVIOUS_TEXT = false;
private static final boolean DEBUG_BATCH_NESTING = false;
// Provision for a long word pair and a separator
- private static final int LOOKBACK_CHARACTER_NUM = Constants.Dictionary.MAX_WORD_LENGTH * 2 + 1;
+ private static final int LOOKBACK_CHARACTER_NUM = Constants.DICTIONARY_MAX_WORD_LENGTH * 2 + 1;
private static final Pattern spaceRegex = Pattern.compile("\\s+");
private static final int INVALID_CURSOR_POSITION = -1;
/**
- * This variable contains the value LatinIME thinks the cursor position should be at now.
- * This is a few steps in advance of what the TextView thinks it is, because TextView will
- * only know after the IPC calls gets through.
+ * 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.
*/
- private int mCurrentCursorPosition = INVALID_CURSOR_POSITION; // in chars, not code points
+ private int mExpectedCursorPosition = 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.
@@ -66,9 +74,6 @@ public final class RichInputConnection {
* This contains the currently composing text, as LatinIME thinks the TextView is seeing it.
*/
private final StringBuilder mComposingText = new StringBuilder();
- // A hint on how many characters to cache from the TextView. A good value of this is given by
- // how many characters we need to be able to almost always find the caps mode.
- private static final int DEFAULT_TEXT_CACHE_SIZE = 100;
private final InputMethodService mParent;
InputConnection mIC;
@@ -86,7 +91,8 @@ public final class RichInputConnection {
r.token = 1;
r.flags = 0;
final ExtractedText et = mIC.getExtractedText(r, 0);
- final CharSequence beforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
+ final CharSequence beforeCursor = getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
+ 0);
final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText)
.append(mComposingText);
if (null == et || null == beforeCursor) return;
@@ -97,16 +103,16 @@ public final class RichInputConnection {
final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString()
: beforeCursor.subSequence(beforeCursor.length() - actualLength,
beforeCursor.length()).toString();
- if (et.selectionStart != mCurrentCursorPosition
+ if (et.selectionStart != mExpectedCursorPosition
|| !(reference.equals(internal.toString()))) {
- final String context = "Expected cursor position = " + mCurrentCursorPosition
+ final String context = "Expected cursor position = " + mExpectedCursorPosition
+ "\nActual cursor position = " + et.selectionStart
+ "\nExpected text = " + internal.length() + " " + internal
+ "\nActual text = " + reference.length() + " " + reference;
((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
} else {
- Log.e(TAG, Utils.getStackTrace(2));
- Log.e(TAG, "Exp <> Actual : " + mCurrentCursorPosition + " <> " + et.selectionStart);
+ Log.e(TAG, DebugLogUtils.getStackTrace(2));
+ Log.e(TAG, "Exp <> Actual : " + mExpectedCursorPosition + " <> " + et.selectionStart);
}
}
@@ -135,26 +141,63 @@ public final class RichInputConnection {
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
}
- public void resetCachesUponCursorMove(final int newCursorPosition,
+ /**
+ * Reset the cached text and retrieve it again from the editor.
+ *
+ * This should be called when the cursor moved. It's possible that we can't connect to
+ * the application when doing this; notably, this happens sometimes during rotation, probably
+ * because of a race condition in the framework. In this case, we just can't retrieve the
+ * 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.
+ * @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) {
- mCurrentCursorPosition = newCursorPosition;
+ mExpectedCursorPosition = newCursorPosition;
mComposingText.setLength(0);
mCommittedTextBeforeComposingText.setLength(0);
- final CharSequence textBeforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
- if (null != textBeforeCursor) mCommittedTextBeforeComposingText.append(textBeforeCursor);
+ mIC = mParent.getCurrentInputConnection();
+ // Call upon the inputconnection directly since our own method is using the cache, and
+ // we want to refresh it.
+ final CharSequence textBeforeCursor = null == mIC ? null :
+ mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
+ 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");
+ 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;
}
private void checkBatchEdit() {
if (mNestLevel != 1) {
// TODO: exception instead
Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
- Log.e(TAG, Utils.getStackTrace(4));
+ Log.e(TAG, DebugLogUtils.getStackTrace(4));
}
}
@@ -162,7 +205,6 @@ public final class RichInputConnection {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
mCommittedTextBeforeComposingText.append(mComposingText);
- mCurrentCursorPosition += mComposingText.length();
mComposingText.setLength(0);
if (null != mIC) {
mIC.finishComposingText();
@@ -176,7 +218,7 @@ public final class RichInputConnection {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
mCommittedTextBeforeComposingText.append(text);
- mCurrentCursorPosition += text.length() - mComposingText.length();
+ mExpectedCursorPosition += text.length() - mComposingText.length();
mComposingText.setLength(0);
if (null != mIC) {
mIC.commitText(text, i);
@@ -188,6 +230,10 @@ public final class RichInputConnection {
return mIC.getSelectedText(flags);
}
+ public boolean canDeleteCharacters() {
+ return mExpectedCursorPosition > 0;
+ }
+
/**
* Gets the caps modes we should be in after this specific string.
*
@@ -222,9 +268,12 @@ 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 != mCurrentCursorPosition) {
- mCommittedTextBeforeComposingText.append(
- getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
+ if (TextUtils.isEmpty(mCommittedTextBeforeComposingText) && 0 != mExpectedCursorPosition) {
+ final CharSequence textBeforeCursor = getTextBeforeCursor(
+ Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
+ if (!TextUtils.isEmpty(textBeforeCursor)) {
+ mCommittedTextBeforeComposingText.append(textBeforeCursor);
+ }
}
// This never calls InputConnection#getCapsMode - in fact, it's a static method that
// never blocks or initiates IPC.
@@ -238,22 +287,35 @@ public final class RichInputConnection {
mCommittedTextBeforeComposingText.length());
}
- public CharSequence getTextBeforeCursor(final int i, final int j) {
- // TODO: use mCommittedTextBeforeComposingText if possible to improve performance
+ public CharSequence getTextBeforeCursor(final int n, final int flags) {
+ final int cachedLength =
+ mCommittedTextBeforeComposingText.length() + mComposingText.length();
+ // If we have enough characters to satisfy the request, or if we have all characters in
+ // the text field, then we can return the cached version right away.
+ if (cachedLength >= n || cachedLength >= mExpectedCursorPosition) {
+ final StringBuilder s = new StringBuilder(mCommittedTextBeforeComposingText);
+ s.append(mComposingText);
+ if (s.length() > n) {
+ s.delete(0, s.length() - n);
+ }
+ return s;
+ }
mIC = mParent.getCurrentInputConnection();
- if (null != mIC) return mIC.getTextBeforeCursor(i, j);
+ if (null != mIC) {
+ return mIC.getTextBeforeCursor(n, flags);
+ }
return null;
}
- public CharSequence getTextAfterCursor(final int i, final int j) {
+ public CharSequence getTextAfterCursor(final int n, final int flags) {
mIC = mParent.getCurrentInputConnection();
- if (null != mIC) return mIC.getTextAfterCursor(i, j);
+ if (null != mIC) return mIC.getTextAfterCursor(n, flags);
return null;
}
- public void deleteSurroundingText(final int i, final int j) {
+ public void deleteSurroundingText(final int beforeLength, final int afterLength) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
- final int remainingChars = mComposingText.length() - i;
+ final int remainingChars = mComposingText.length() - beforeLength;
if (remainingChars >= 0) {
mComposingText.setLength(remainingChars);
} else {
@@ -263,15 +325,15 @@ public final class RichInputConnection {
+ remainingChars, 0);
mCommittedTextBeforeComposingText.setLength(len);
}
- if (mCurrentCursorPosition > i) {
- mCurrentCursorPosition -= i;
+ if (mExpectedCursorPosition > beforeLength) {
+ mExpectedCursorPosition -= beforeLength;
} else {
- mCurrentCursorPosition = 0;
+ mExpectedCursorPosition = 0;
}
if (null != mIC) {
- mIC.deleteSurroundingText(i, j);
+ mIC.deleteSurroundingText(beforeLength, afterLength);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.richInputConnection_deleteSurroundingText(i, j);
+ ResearchLogger.richInputConnection_deleteSurroundingText(beforeLength, afterLength);
}
}
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
@@ -300,7 +362,7 @@ public final class RichInputConnection {
switch (keyEvent.getKeyCode()) {
case KeyEvent.KEYCODE_ENTER:
mCommittedTextBeforeComposingText.append("\n");
- mCurrentCursorPosition += 1;
+ mExpectedCursorPosition += 1;
break;
case KeyEvent.KEYCODE_DEL:
if (0 == mComposingText.length()) {
@@ -312,18 +374,18 @@ public final class RichInputConnection {
} else {
mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
}
- if (mCurrentCursorPosition > 0) mCurrentCursorPosition -= 1;
+ if (mExpectedCursorPosition > 0) mExpectedCursorPosition -= 1;
break;
case KeyEvent.KEYCODE_UNKNOWN:
if (null != keyEvent.getCharacters()) {
mCommittedTextBeforeComposingText.append(keyEvent.getCharacters());
- mCurrentCursorPosition += keyEvent.getCharacters().length();
+ mExpectedCursorPosition += keyEvent.getCharacters().length();
}
break;
default:
final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1);
mCommittedTextBeforeComposingText.append(text);
- mCurrentCursorPosition += text.length();
+ mExpectedCursorPosition += text.length();
break;
}
}
@@ -338,9 +400,8 @@ public final class RichInputConnection {
public void setComposingRegion(final int start, final int end) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
- mCurrentCursorPosition = end;
final CharSequence textBeforeCursor =
- getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE + (end - start), 0);
+ getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE + (end - start), 0);
mCommittedTextBeforeComposingText.setLength(0);
if (!TextUtils.isEmpty(textBeforeCursor)) {
final int indexOfStartOfComposingText =
@@ -355,34 +416,35 @@ public final class RichInputConnection {
}
}
- public void setComposingText(final CharSequence text, final int i) {
+ public void setComposingText(final CharSequence text, final int newCursorPosition) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
- mCurrentCursorPosition += text.length() - mComposingText.length();
+ mExpectedCursorPosition += text.length() - mComposingText.length();
mComposingText.setLength(0);
mComposingText.append(text);
// TODO: support values of i != 1. At this time, this is never called with i != 1.
if (null != mIC) {
- mIC.setComposingText(text, i);
+ mIC.setComposingText(text, newCursorPosition);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.richInputConnection_setComposingText(text, i);
+ ResearchLogger.richInputConnection_setComposingText(text, newCursorPosition);
}
}
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
}
- public void setSelection(final int from, final int to) {
+ public void setSelection(final int start, final int end) {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
if (null != mIC) {
- mIC.setSelection(from, to);
+ mIC.setSelection(start, end);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
- ResearchLogger.richInputConnection_setSelection(from, to);
+ ResearchLogger.richInputConnection_setSelection(start, end);
}
}
- mCurrentCursorPosition = from;
+ mExpectedCursorPosition = start;
mCommittedTextBeforeComposingText.setLength(0);
- mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
+ mCommittedTextBeforeComposingText.append(
+ getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0));
}
public void commitCorrection(final CorrectionInfo correctionInfo) {
@@ -403,7 +465,7 @@ 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);
- mCurrentCursorPosition += text.length() - mComposingText.length();
+ mExpectedCursorPosition += text.length() - mComposingText.length();
mComposingText.setLength(0);
if (null != mIC) {
mIC.commitCompletion(completionInfo);
@@ -418,7 +480,7 @@ public final class RichInputConnection {
public String getNthPreviousWord(final String sentenceSeperators, final int n) {
mIC = mParent.getCurrentInputConnection();
if (null == mIC) return null;
- final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
+ final CharSequence prev = getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
if (DEBUG_PREVIOUS_TEXT && null != prev) {
final int checkLength = LOOKBACK_CHARACTER_NUM - 1;
final String reference = prev.length() <= checkLength ? prev.toString()
@@ -437,32 +499,6 @@ public final class RichInputConnection {
return getNthPreviousWord(prev, sentenceSeperators, n);
}
- /**
- * Represents a range of text, relative to the current cursor position.
- */
- public static final class Range {
- /** Characters before selection start */
- public final int mCharsBefore;
-
- /**
- * Characters after selection start, including one trailing word
- * separator.
- */
- public final int mCharsAfter;
-
- /** The actual characters that make up a word */
- public final CharSequence mWord;
-
- public Range(int charsBefore, int charsAfter, CharSequence word) {
- if (charsBefore < 0 || charsAfter < 0) {
- throw new IndexOutOfBoundsException();
- }
- this.mCharsBefore = charsBefore;
- this.mCharsAfter = charsAfter;
- this.mWord = word;
- }
- }
-
private static boolean isSeparator(int code, String sep) {
return sep.indexOf(code) != -1;
}
@@ -509,7 +545,7 @@ public final class RichInputConnection {
*/
public CharSequence getWordAtCursor(String separators) {
// getWordRangeAtCursor returns null if the connection is null
- Range r = getWordRangeAtCursor(separators, 0);
+ TextRange r = getWordRangeAtCursor(separators, 0);
return (r == null) ? null : r.mWord;
}
@@ -521,14 +557,15 @@ public final class RichInputConnection {
* be included in the returned range
* @return a range containing the text surrounding the cursor
*/
- public Range getWordRangeAtCursor(final String sep, final int additionalPrecedingWordsCount) {
+ public TextRange getWordRangeAtCursor(final String sep,
+ final int additionalPrecedingWordsCount) {
mIC = mParent.getCurrentInputConnection();
if (mIC == null || sep == null) {
return null;
}
- final CharSequence before = mIC.getTextBeforeCursor(1000,
+ final CharSequence before = mIC.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
InputConnection.GET_TEXT_WITH_STYLES);
- final CharSequence after = mIC.getTextAfterCursor(1000,
+ final CharSequence after = mIC.getTextAfterCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE,
InputConnection.GET_TEXT_WITH_STYLES);
if (before == null || after == null) {
return null;
@@ -571,19 +608,22 @@ public final class RichInputConnection {
}
}
- final SpannableString word = new SpannableString(TextUtils.concat(
- before.subSequence(startIndexInBefore, before.length()),
- after.subSequence(0, endIndexInAfter)));
- return new Range(before.length() - startIndexInBefore, endIndexInAfter, word);
+ // 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());
}
public boolean isCursorTouchingWord(final SettingsValues settingsValues) {
- final CharSequence before = getTextBeforeCursor(1, 0);
- final CharSequence after = getTextAfterCursor(1, 0);
- if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0))
- && !settingsValues.isWordConnector(before.charAt(0))) {
+ final int codePointBeforeCursor = getCodePointBeforeCursor();
+ if (Constants.NOT_A_CODE != codePointBeforeCursor
+ && !settingsValues.isWordSeparator(codePointBeforeCursor)
+ && !settingsValues.isWordConnector(codePointBeforeCursor)) {
return true;
}
+ final CharSequence after = getTextAfterCursor(1, 0);
if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0))
&& !settingsValues.isWordConnector(after.charAt(0))) {
return true;
@@ -593,9 +633,8 @@ public final class RichInputConnection {
public void removeTrailingSpace() {
if (DEBUG_BATCH_NESTING) checkBatchEdit();
- final CharSequence lastOne = getTextBeforeCursor(1, 0);
- if (lastOne != null && lastOne.length() == 1
- && lastOne.charAt(0) == Constants.CODE_SPACE) {
+ final int codePointBeforeCursor = getCodePointBeforeCursor();
+ if (Constants.CODE_SPACE == codePointBeforeCursor) {
deleteSurroundingText(1, 0);
}
}
@@ -650,7 +689,7 @@ public final class RichInputConnection {
// be needed, but it's there just in case something went wrong.
final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
final String periodSpace = ". ";
- if (!periodSpace.equals(textBeforeCursor)) {
+ if (!TextUtils.equals(periodSpace, textBeforeCursor)) {
// Theoretically we should not be coming here if there isn't ". " before the
// cursor, but the application may be changing the text while we are typing, so
// anything goes. We should not crash.
@@ -658,9 +697,11 @@ public final class RichInputConnection {
+ "\"" + periodSpace + "\" just before the cursor.");
return false;
}
+ // Double-space results in ". ". A backspace to cancel this should result in a single
+ // space in the text field, so we replace ". " with a single space.
deleteSurroundingText(2, 0);
- final String doubleSpace = " ";
- commitText(doubleSpace, 1);
+ final String singleSpace = " ";
+ commitText(singleSpace, 1);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
ResearchLogger.richInputConnection_revertDoubleSpacePeriod();
}
@@ -712,14 +753,14 @@ public final class RichInputConnection {
*/
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 == mCurrentCursorPosition) return true;
+ 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 == mCurrentCursorPosition) return false;
+ 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) * (mCurrentCursorPosition - newSelStart) >= 0;
+ return (newSelStart - oldSelStart) * (mExpectedCursorPosition - newSelStart) >= 0;
}
/**
diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
index 0dd302afa..6b6bbf3a7 100644
--- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
+++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
@@ -28,8 +28,13 @@ import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.latin.settings.Settings;
+import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
/**
@@ -46,6 +51,10 @@ public final class RichInputMethodManager {
private InputMethodManagerCompatWrapper mImmWrapper;
private InputMethodInfo mInputMethodInfoOfThisIme;
+ final HashMap<InputMethodInfo, List<InputMethodSubtype>>
+ mSubtypeListCacheWithImplicitlySelectedSubtypes = CollectionUtils.newHashMap();
+ final HashMap<InputMethodInfo, List<InputMethodSubtype>>
+ mSubtypeListCacheWithoutImplicitlySelectedSubtypes = CollectionUtils.newHashMap();
private static final int INDEX_NOT_FOUND = -1;
@@ -54,13 +63,6 @@ public final class RichInputMethodManager {
return sInstance;
}
- // Caveat: This may cause IPC
- public static boolean isInputMethodManagerValidForUserOfThisProcess(final Context context) {
- // Basically called to check whether this IME has been triggered by the current user or not
- return !((InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE)).
- getInputMethodList().isEmpty();
- }
-
public static void init(final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
sInstance.initInternal(context, prefs);
@@ -84,11 +86,11 @@ public final class RichInputMethodManager {
mInputMethodInfoOfThisIme = getInputMethodInfoOfThisIme(context);
// Initialize additional subtypes.
- SubtypeLocale.init(context);
+ SubtypeLocaleUtils.init(context);
final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes(
prefs, context.getResources());
final InputMethodSubtype[] additionalSubtypes =
- AdditionalSubtype.createAdditionalSubtypesArray(prefAdditionalSubtypes);
+ AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefAdditionalSubtypes);
setAdditionalInputMethodSubtypes(additionalSubtypes);
}
@@ -109,8 +111,8 @@ public final class RichInputMethodManager {
public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList(
boolean allowsImplicitlySelectedSubtypes) {
- return mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
- mInputMethodInfoOfThisIme, allowsImplicitlySelectedSubtypes);
+ return getEnabledInputMethodSubtypeList(mInputMethodInfoOfThisIme,
+ allowsImplicitlySelectedSubtypes);
}
public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) {
@@ -134,7 +136,7 @@ public final class RichInputMethodManager {
final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
if (currentIndex == INDEX_NOT_FOUND) {
Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
- + SubtypeLocale.getSubtypeDisplayName(currentSubtype));
+ + SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype));
return false;
}
final int nextIndex = (currentIndex + 1) % enabledSubtypes.size();
@@ -158,8 +160,8 @@ public final class RichInputMethodManager {
return false;
}
final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis);
- final List<InputMethodSubtype> enabledSubtypes = imm.getEnabledInputMethodSubtypeList(
- nextImi, true /* allowsImplicitlySelectedSubtypes */);
+ final List<InputMethodSubtype> enabledSubtypes = getEnabledInputMethodSubtypeList(nextImi,
+ true /* allowsImplicitlySelectedSubtypes */);
if (enabledSubtypes.isEmpty()) {
// The next IME has no subtype.
imm.setInputMethod(token, nextImi.getId());
@@ -234,9 +236,8 @@ public final class RichInputMethodManager {
public boolean checkIfSubtypeBelongsToImeAndEnabled(final InputMethodInfo imi,
final InputMethodSubtype subtype) {
- return checkIfSubtypeBelongsToList(
- subtype, mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
- imi, true /* allowsImplicitlySelectedSubtypes */));
+ return checkIfSubtypeBelongsToList(subtype, getEnabledInputMethodSubtypeList(imi,
+ true /* allowsImplicitlySelectedSubtypes */));
}
private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype,
@@ -297,8 +298,7 @@ public final class RichInputMethodManager {
for (InputMethodInfo imi : imiList) {
// We can return true immediately after we find two or more filtered IMEs.
if (filteredImisCount > 1) return true;
- final List<InputMethodSubtype> subtypes =
- mImmWrapper.mImm.getEnabledInputMethodSubtypeList(imi, true);
+ final List<InputMethodSubtype> subtypes = getEnabledInputMethodSubtypeList(imi, true);
// IMEs that have no subtypes should be counted.
if (subtypes.isEmpty()) {
++filteredImisCount;
@@ -344,7 +344,7 @@ public final class RichInputMethodManager {
final int count = myImi.getSubtypeCount();
for (int i = 0; i < count; i++) {
final InputMethodSubtype subtype = myImi.getSubtypeAt(i);
- final String layoutName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
+ final String layoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
if (localeString.equals(subtype.getLocale())
&& keyboardLayoutSetName.equals(layoutName)) {
return subtype;
@@ -361,5 +361,26 @@ public final class RichInputMethodManager {
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.
+ clearSubtypeCaches();
+ }
+
+ private List<InputMethodSubtype> getEnabledInputMethodSubtypeList(final InputMethodInfo imi,
+ final boolean allowsImplicitlySelectedSubtypes) {
+ final HashMap<InputMethodInfo, List<InputMethodSubtype>> cache =
+ allowsImplicitlySelectedSubtypes
+ ? mSubtypeListCacheWithImplicitlySelectedSubtypes
+ : mSubtypeListCacheWithoutImplicitlySelectedSubtypes;
+ final List<InputMethodSubtype> cachedList = cache.get(imi);
+ if (null != cachedList) return cachedList;
+ final List<InputMethodSubtype> result = mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
+ imi, allowsImplicitlySelectedSubtypes);
+ cache.put(imi, result);
+ return result;
+ }
+
+ public void clearSubtypeCaches() {
+ mSubtypeListCacheWithImplicitlySelectedSubtypes.clear();
+ mSubtypeListCacheWithoutImplicitlySelectedSubtypes.clear();
}
}
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index 282b5794f..cd9c89f04 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -33,6 +33,7 @@ import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
import java.util.List;
import java.util.Locale;
@@ -43,20 +44,36 @@ public final class SubtypeSwitcher {
private static final String TAG = SubtypeSwitcher.class.getSimpleName();
private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
+
private /* final */ RichInputMethodManager mRichImm;
private /* final */ Resources mResources;
private /* final */ ConnectivityManager mConnectivityManager;
- /*-----------------------------------------------------------*/
- // Variants which should be changed only by reload functions.
- private NeedsToDisplayLanguage mNeedsToDisplayLanguage = new NeedsToDisplayLanguage();
+ private final NeedsToDisplayLanguage mNeedsToDisplayLanguage = new NeedsToDisplayLanguage();
private InputMethodInfo mShortcutInputMethodInfo;
private InputMethodSubtype mShortcutSubtype;
private InputMethodSubtype mNoLanguageSubtype;
- /*-----------------------------------------------------------*/
-
+ private InputMethodSubtype mEmojiSubtype;
private boolean mIsNetworkConnected;
+ // 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 */);
+ // 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;
@@ -79,7 +96,7 @@ public final class SubtypeSwitcher {
}
public static void init(final Context context) {
- SubtypeLocale.init(context);
+ SubtypeLocaleUtils.init(context);
RichInputMethodManager.init(context);
sInstance.initialize(context);
}
@@ -96,11 +113,6 @@ public final class SubtypeSwitcher {
mRichImm = RichInputMethodManager.getInstance();
mConnectivityManager = (ConnectivityManager) context.getSystemService(
Context.CONNECTIVITY_SERVICE);
- mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
- SubtypeLocale.NO_LANGUAGE, SubtypeLocale.QWERTY);
- if (mNoLanguageSubtype == null) {
- throw new RuntimeException("Can't find no lanugage with QWERTY subtype");
- }
final NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
mIsNetworkConnected = (info != null && info.isConnected());
@@ -155,10 +167,11 @@ public final class SubtypeSwitcher {
// Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function.
public void onSubtypeChanged(final InputMethodSubtype newSubtype) {
if (DBG) {
- Log.w(TAG, "onSubtypeChanged: " + SubtypeLocale.getSubtypeDisplayName(newSubtype));
+ Log.w(TAG, "onSubtypeChanged: "
+ + SubtypeLocaleUtils.getSubtypeNameForLogging(newSubtype));
}
- final Locale newLocale = SubtypeLocale.getSubtypeLocale(newSubtype);
+ final Locale newLocale = SubtypeLocaleUtils.getSubtypeLocale(newSubtype);
final Locale systemLocale = mResources.getConfiguration().locale;
final boolean sameLocale = systemLocale.equals(newLocale);
final boolean sameLanguage = systemLocale.getLanguage().equals(newLocale.getLanguage());
@@ -234,7 +247,7 @@ public final class SubtypeSwitcher {
//////////////////////////////////
public boolean needsToDisplayLanguage(final Locale keyboardLocale) {
- if (keyboardLocale.toString().equals(SubtypeLocale.NO_LANGUAGE)) {
+ if (keyboardLocale.toString().equals(SubtypeLocaleUtils.NO_LANGUAGE)) {
return true;
}
if (!keyboardLocale.equals(getCurrentSubtypeLocale())) {
@@ -251,14 +264,37 @@ public final class SubtypeSwitcher {
public Locale getCurrentSubtypeLocale() {
if (null != sForcedLocaleForTesting) return sForcedLocaleForTesting;
- return SubtypeLocale.getSubtypeLocale(getCurrentSubtype());
+ return SubtypeLocaleUtils.getSubtypeLocale(getCurrentSubtype());
}
public InputMethodSubtype getCurrentSubtype() {
- return mRichImm.getCurrentInputMethodSubtype(mNoLanguageSubtype);
+ return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype());
}
public InputMethodSubtype getNoLanguageSubtype() {
- return mNoLanguageSubtype;
+ if (mNoLanguageSubtype == null) {
+ mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+ SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
+ }
+ 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: "
+ + DUMMY_NO_LANGUAGE_SUBTYPE);
+ return DUMMY_NO_LANGUAGE_SUBTYPE;
+ }
+
+ public InputMethodSubtype getEmojiSubtype() {
+ if (mEmojiSubtype == null) {
+ mEmojiSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+ SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
+ }
+ 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);
+ 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 e783e6d51..6c18c948f 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -17,11 +17,21 @@
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.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.UserHistoryPredictionDictionary;
+import com.android.inputmethod.latin.settings.Settings;
+import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
+import com.android.inputmethod.latin.utils.BoundedTreeSet;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
import java.util.ArrayList;
import java.util.Comparator;
@@ -38,8 +48,9 @@ public final class Suggest {
// Session id for
// {@link #getSuggestedWords(WordComposer,String,ProximityInfo,boolean,int)}.
+ // We are sharing the same ID between typing and gesture to save RAM footprint.
public static final int SESSION_TYPING = 0;
- public static final int SESSION_GESTURE = 1;
+ public static final int SESSION_GESTURE = 0;
// TODO: rename this to CORRECTION_OFF
public static final int CORRECTION_NONE = 0;
@@ -49,21 +60,22 @@ 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 Dictionary mMainDictionary;
- private ContactsBinaryDictionary mContactsDict;
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 static final int MAX_SUGGESTIONS = 18;
-
private float mAutoCorrectionThreshold;
// Locale used for upper- and title-casing words
@@ -73,6 +85,13 @@ public final class Suggest {
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
@@ -81,7 +100,7 @@ public final class Suggest {
false /* useFullEditDistance */, locale);
mLocale = locale;
mMainDictionary = mainDict;
- addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, mainDict);
+ addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, mainDict);
}
private void initAsynchronously(final Context context, final Locale locale,
@@ -89,6 +108,14 @@ public final class Suggest {
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) {
@@ -112,7 +139,7 @@ public final class Suggest {
public void run() {
final DictionaryCollection newMainDict =
DictionaryFactory.createMainDictionaryFromManager(context, locale);
- addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, newMainDict);
+ addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, newMainDict);
mMainDictionary = newMainDict;
if (listener != null) {
listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
@@ -150,7 +177,7 @@ public final class Suggest {
* before the main dictionary, if set. This refers to the system-managed user dictionary.
*/
public void setUserDictionary(final UserBinaryDictionary userDictionary) {
- addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_USER, userDictionary);
+ addOrReplaceDictionaryInternal(Dictionary.TYPE_USER, userDictionary);
}
/**
@@ -160,35 +187,56 @@ public final class Suggest {
*/
public void setContactsDictionary(final ContactsBinaryDictionary contactsDictionary) {
mContactsDict = contactsDictionary;
- addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_CONTACTS, contactsDictionary);
+ addOrReplaceDictionaryInternal(Dictionary.TYPE_CONTACTS, contactsDictionary);
+ }
+
+ public void setUserHistoryPredictionDictionary(
+ final UserHistoryPredictionDictionary userHistoryPredictionDictionary) {
+ addOrReplaceDictionaryInternal(Dictionary.TYPE_USER_HISTORY,
+ userHistoryPredictionDictionary);
+ }
+
+ public void setPersonalizationPredictionDictionary(
+ final PersonalizationPredictionDictionary personalizationPredictionDictionary) {
+ addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION_PREDICTION_IN_JAVA,
+ personalizationPredictionDictionary);
}
- public void setUserHistoryDictionary(final UserHistoryDictionary userHistoryDictionary) {
- addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_USER_HISTORY, userHistoryDictionary);
+ public void setPersonalizationDictionary(
+ final PersonalizationDictionary personalizationDictionary) {
+ addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION,
+ personalizationDictionary);
}
public void setAutoCorrectionThreshold(float threshold) {
mAutoCorrectionThreshold = threshold;
}
- public SuggestedWords getSuggestedWords(final WordComposer wordComposer,
+ public interface OnGetSuggestedWordsCallback {
+ public void onGetSuggestedWords(final SuggestedWords suggestedWords);
+ }
+
+ public void getSuggestedWords(final WordComposer wordComposer,
final String prevWordForBigram, final ProximityInfo proximityInfo,
final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
- final int sessionId) {
+ final int[] additionalFeaturesOptions, final int sessionId,
+ final OnGetSuggestedWordsCallback callback) {
LatinImeLogger.onStartSuggestion(prevWordForBigram);
if (wordComposer.isBatchMode()) {
- return getSuggestedWordsForBatchInput(
- wordComposer, prevWordForBigram, proximityInfo, blockOffensiveWords, sessionId);
+ getSuggestedWordsForBatchInput(wordComposer, prevWordForBigram, proximityInfo,
+ blockOffensiveWords, additionalFeaturesOptions, sessionId, callback);
} else {
- return getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo,
- blockOffensiveWords, isCorrectionEnabled);
+ getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo,
+ blockOffensiveWords, isCorrectionEnabled, additionalFeaturesOptions, callback);
}
}
- // Retrieves suggestions for the typing input.
- private SuggestedWords getSuggestedWordsForTypingInput(final WordComposer wordComposer,
+ // Retrieves suggestions for the typing input
+ // and calls the callback function with the suggestions.
+ private void getSuggestedWordsForTypingInput(final WordComposer wordComposer,
final String prevWordForBigram, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords, final boolean isCorrectionEnabled) {
+ final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
+ final int[] additionalFeaturesOptions, final OnGetSuggestedWordsCallback callback) {
final int trailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
MAX_SUGGESTIONS);
@@ -211,8 +259,9 @@ public final class Suggest {
for (final String key : mDictionaries.keySet()) {
final Dictionary dictionary = mDictionaries.get(key);
- suggestionsSet.addAll(dictionary.getSuggestions(
- wordComposerForLookup, prevWordForBigram, proximityInfo, blockOffensiveWords));
+ suggestionsSet.addAll(dictionary.getSuggestions(wordComposerForLookup,
+ prevWordForBigram, proximityInfo, blockOffensiveWords,
+ additionalFeaturesOptions));
}
final String whitelistedWord;
@@ -228,7 +277,7 @@ public final class Suggest {
// or if it's a 2+ characters non-word (i.e. it's not in the dictionary).
final boolean allowsToBeAutoCorrected = (null != whitelistedWord
&& !whitelistedWord.equals(consideredWord))
- || (consideredWord.length() > 1 && !AutoCorrection.isValidWord(this,
+ || (consideredWord.length() > 1 && !AutoCorrectionUtils.isValidWord(this,
consideredWord, wordComposer.isFirstCharCapitalized()));
final boolean hasAutoCorrection;
@@ -249,7 +298,7 @@ public final class Suggest {
// auto-correct.
hasAutoCorrection = false;
} else {
- hasAutoCorrection = AutoCorrection.suggestionExceedsAutoCorrectionThreshold(
+ hasAutoCorrection = AutoCorrectionUtils.suggestionExceedsAutoCorrectionThreshold(
suggestionsSet.first(), consideredWord, mAutoCorrectionThreshold);
}
@@ -270,13 +319,16 @@ public final class Suggest {
for (int i = 0; i < suggestionsCount; ++i) {
final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
- LatinImeLogger.onAddSuggestedWord(wordInfo.mWord.toString(), wordInfo.mSourceDict);
+ LatinImeLogger.onAddSuggestedWord(wordInfo.mWord.toString(),
+ wordInfo.mSourceDict.mDictType);
}
if (!TextUtils.isEmpty(typedWord)) {
suggestionsContainer.add(0, new SuggestedWordInfo(typedWord,
SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED,
- Dictionary.TYPE_USER_TYPED));
+ Dictionary.DICTIONARY_USER_TYPED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+ SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
}
SuggestedWordInfo.removeDups(suggestionsContainer);
@@ -287,7 +339,7 @@ public final class Suggest {
suggestionsList = suggestionsContainer;
}
- return new SuggestedWords(suggestionsList,
+ callback.onGetSuggestedWords(new SuggestedWords(suggestionsList,
// 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.
@@ -295,31 +347,28 @@ public final class Suggest {
hasAutoCorrection, /* willAutoCorrect */
false /* isPunctuationSuggestions */,
false /* isObsoleteSuggestions */,
- !wordComposer.isComposingWord() /* isPrediction */);
+ !wordComposer.isComposingWord() /* isPrediction */));
}
- // Retrieves suggestions for the batch input.
- private SuggestedWords getSuggestedWordsForBatchInput(final WordComposer wordComposer,
+ // Retrieves suggestions for the batch input
+ // and calls the callback function with the suggestions.
+ private void getSuggestedWordsForBatchInput(final WordComposer wordComposer,
final String prevWordForBigram, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords, final int sessionId) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+ final int sessionId, 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()) {
- // Skip User history dictionary for lookup
- // TODO: The user history dictionary should just override getSuggestionsWithSessionId
- // to make sure it doesn't return anything and we should remove this test
- if (key.equals(Dictionary.TYPE_USER_HISTORY)) {
- continue;
- }
final Dictionary dictionary = mDictionaries.get(key);
suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId(wordComposer,
- prevWordForBigram, proximityInfo, blockOffensiveWords, sessionId));
+ prevWordForBigram, proximityInfo, blockOffensiveWords,
+ additionalFeaturesOptions, sessionId));
}
for (SuggestedWordInfo wordInfo : suggestionsSet) {
- LatinImeLogger.onAddSuggestedWord(wordInfo.mWord, wordInfo.mSourceDict);
+ LatinImeLogger.onAddSuggestedWord(wordInfo.mWord, wordInfo.mSourceDict.mDictType);
}
final ArrayList<SuggestedWordInfo> suggestionsContainer =
@@ -354,12 +403,12 @@ 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).
- return new SuggestedWords(suggestionsContainer,
+ callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer,
true /* typedWordValid */,
false /* willAutoCorrect */,
false /* isPunctuationSuggestions */,
false /* isObsoleteSuggestions */,
- false /* isPrediction */);
+ false /* isPrediction */));
}
private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
@@ -405,7 +454,7 @@ public final class Suggest {
private static final SuggestedWordInfoComparator sSuggestedWordInfoComparator =
new SuggestedWordInfoComparator();
- private static SuggestedWordInfo getTransformedSuggestedWordInfo(
+ /* package for test */ static SuggestedWordInfo getTransformedSuggestedWordInfo(
final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase,
final boolean isFirstCharCapitalized, final int trailingSingleQuotesCount) {
final StringBuilder sb = new StringBuilder(wordInfo.mWord.length());
@@ -416,11 +465,17 @@ public final class Suggest {
} else {
sb.append(wordInfo.mWord);
}
- for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) {
+ // Appending quotes is here to help people quote words. However, it's not helpful
+ // when they type words with quotes toward the end like "it's" or "didn't", where
+ // it's more likely the user missed the last character (or didn't type it yet).
+ final int quotesToAppend = trailingSingleQuotesCount
+ - (-1 == wordInfo.mWord.indexOf(Constants.CODE_SINGLE_QUOTE) ? 0 : 1);
+ for (int i = quotesToAppend - 1; i >= 0; --i) {
sb.appendCodePoint(Constants.CODE_SINGLE_QUOTE);
}
return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKind,
- wordInfo.mSourceDict);
+ wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord,
+ SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */);
}
public void close() {
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 7a16595a7..fed4cdbbb 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -19,6 +19,9 @@ package com.android.inputmethod.latin;
import android.text.TextUtils;
import android.view.inputmethod.CompletionInfo;
+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.HashSet;
@@ -72,6 +75,21 @@ public final class SuggestedWords {
return mSuggestedWordInfoList.get(index);
}
+ public String getDebugString(final int pos) {
+ if (!LatinImeLogger.sDBG) {
+ return null;
+ }
+ final SuggestedWordInfo wordInfo = getInfo(pos);
+ if (wordInfo == null) {
+ return null;
+ }
+ final String debugString = wordInfo.getDebugString();
+ if (TextUtils.isEmpty(debugString)) {
+ return null;
+ }
+ return debugString;
+ }
+
public boolean willAutoCorrect() {
return mWillAutoCorrect;
}
@@ -95,7 +113,9 @@ public final class SuggestedWords {
if (null == text) continue;
final SuggestedWordInfo suggestedWordInfo = new SuggestedWordInfo(text.toString(),
SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_APP_DEFINED,
- Dictionary.TYPE_APPLICATION_DEFINED);
+ Dictionary.DICTIONARY_APPLICATION_DEFINED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+ SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */);
result.add(suggestedWordInfo);
}
return result;
@@ -108,7 +128,9 @@ public final class SuggestedWords {
final ArrayList<SuggestedWordInfo> suggestionsList = CollectionUtils.newArrayList();
final HashSet<String> alreadySeen = CollectionUtils.newHashSet();
suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE,
- SuggestedWordInfo.KIND_TYPED, Dictionary.TYPE_USER_TYPED));
+ SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+ SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
alreadySeen.add(typedWord.toString());
final int previousSize = previousSuggestions.size();
for (int index = 1; index < previousSize; index++) {
@@ -123,7 +145,15 @@ public final class SuggestedWords {
return suggestionsList;
}
+ public SuggestedWordInfo getAutoCommitCandidate() {
+ if (mSuggestedWordInfoList.size() <= 0) return null;
+ final SuggestedWordInfo candidate = mSuggestedWordInfoList.get(0);
+ return candidate.isEligibleForAutoCommit() ? candidate : null;
+ }
+
public static final class SuggestedWordInfo {
+ public static final int NOT_AN_INDEX = -1;
+ public static final int NOT_A_CONFIDENCE = -1;
public static final int MAX_SCORE = Integer.MAX_VALUE;
public static final int KIND_MASK_KIND = 0xFF; // Mask to get only the kind
public static final int KIND_TYPED = 0; // What user typed
@@ -148,18 +178,40 @@ public final class SuggestedWords {
public final int mScore;
public final int mKind; // one of the KIND_* constants above
public final int mCodePointCount;
- public final String mSourceDict;
+ public final Dictionary mSourceDict;
+ // For auto-commit. This keeps track of the index inside the touch coordinates array
+ // passed to native code to get suggestions for a gesture that corresponds to the first
+ // letter of the second word.
+ public final int mIndexOfTouchPointOfSecondWord;
+ // For auto-commit. This is a measure of how confident we are that we can commit the
+ // first word of this suggestion.
+ public final int mAutoCommitFirstWordConfidence;
private String mDebugString = "";
+ /**
+ * Create a new suggested word info.
+ * @param word The string to suggest.
+ * @param score A measure of how likely this suggestion is.
+ * @param kind The kind of suggestion, as one of the above KIND_* constants.
+ * @param sourceDict What instance of Dictionary produced this suggestion.
+ * @param indexOfTouchPointOfSecondWord See mIndexOfTouchPointOfSecondWord.
+ * @param autoCommitFirstWordConfidence See mAutoCommitFirstWordConfidence.
+ */
public SuggestedWordInfo(final String word, final int score, final int kind,
- final String sourceDict) {
+ final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord,
+ final int autoCommitFirstWordConfidence) {
mWord = word;
mScore = score;
mKind = kind;
mSourceDict = sourceDict;
mCodePointCount = StringUtils.codePointCount(mWord);
+ mIndexOfTouchPointOfSecondWord = indexOfTouchPointOfSecondWord;
+ mAutoCommitFirstWordConfidence = autoCommitFirstWordConfidence;
}
+ public boolean isEligibleForAutoCommit() {
+ return (KIND_CORRECTION == mKind && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord);
+ }
public void setDebugString(final String str) {
if (null == str) throw new NullPointerException("Debug info is null");
@@ -224,4 +276,24 @@ public final class SuggestedWords {
false /* willAutoCorrect */, mIsPunctuationSuggestions, mIsObsoleteSuggestions,
mIsPrediction);
}
+
+ // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the
+ // last word of all suggestions, separated by a space. This is necessary because when we commit
+ // a multiple-word suggestion, the IME only retains the last word as the composing word, and
+ // we should only suggest replacements for this last word.
+ // TODO: make this work with languages without spaces.
+ public SuggestedWords getSuggestedWordsForLastWordOfPhraseGesture() {
+ final ArrayList<SuggestedWordInfo> newSuggestions = CollectionUtils.newArrayList();
+ for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) {
+ final SuggestedWordInfo info = mSuggestedWordInfoList.get(i);
+ final int indexOfLastSpace = info.mWord.lastIndexOf(Constants.CODE_SPACE) + 1;
+ final String lastWord = info.mWord.substring(indexOfLastSpace);
+ newSuggestions.add(new SuggestedWordInfo(lastWord, info.mScore, info.mKind,
+ info.mSourceDict, SuggestedWordInfo.NOT_AN_INDEX,
+ SuggestedWordInfo.NOT_A_CONFIDENCE));
+ }
+ return new SuggestedWords(newSuggestions, mTypedWordValid,
+ mWillAutoCorrect, mIsPunctuationSuggestions, mIsObsoleteSuggestions,
+ mIsPrediction);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
index 92f96c027..3213c92c7 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
@@ -34,14 +34,15 @@ public final class SynchronouslyLoadedContactsBinaryDictionary extends ContactsB
@Override
public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
final String prevWordForBigrams, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
- syncReloadDictionaryIfRequired();
- return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords);
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
+ reloadDictionaryIfRequired();
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords,
+ additionalFeaturesOptions);
}
@Override
public synchronized boolean isValidWord(final String word) {
- syncReloadDictionaryIfRequired();
+ reloadDictionaryIfRequired();
return isValidWordInner(word);
}
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
index 33fe89611..6405b5e46 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
@@ -37,14 +37,15 @@ public final class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDic
@Override
public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
final String prevWordForBigrams, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
- syncReloadDictionaryIfRequired();
- return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords);
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
+ reloadDictionaryIfRequired();
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo, blockOffensiveWords,
+ additionalFeaturesOptions);
}
@Override
public synchronized boolean isValidWord(final String word) {
- syncReloadDictionaryIfRequired();
+ reloadDictionaryIfRequired();
return isValidWordInner(word);
}
}
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 90f92972a..864a17375 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -22,12 +22,16 @@ import android.content.ContentUris;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.os.Build;
import android.provider.UserDictionary.Words;
import android.text.TextUtils;
+import android.util.Log;
import com.android.inputmethod.compat.UserDictionaryCompatUtils;
+import com.android.inputmethod.latin.utils.LocaleUtils;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
import java.util.Arrays;
import java.util.Locale;
@@ -37,6 +41,7 @@ import java.util.Locale;
* dictionary file to use it from native code.
*/
public class UserBinaryDictionary extends ExpandableBinaryDictionary {
+ private static final String TAG = ExpandableBinaryDictionary.class.getSimpleName();
// The user dictionary provider uses an empty string to mean "all languages".
private static final String USER_DICTIONARY_ALL_LANGUAGES = "";
@@ -73,9 +78,10 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
public UserBinaryDictionary(final Context context, final String locale,
final boolean alsoUseMoreRestrictiveLocales) {
- super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER);
+ super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER,
+ false /* isUpdatable */);
if (null == locale) throw new NullPointerException(); // Catch the error earlier
- if (SubtypeLocale.NO_LANGUAGE.equals(locale)) {
+ if (SubtypeLocaleUtils.NO_LANGUAGE.equals(locale)) {
// If we don't have a locale, insert into the "all locales" user dictionary.
mLocale = USER_DICTIONARY_ALL_LANGUAGES;
} else {
@@ -100,14 +106,6 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
@Override
public void onChange(final boolean self, final Uri uri) {
setRequiresReload(true);
- // We want to report back to Latin IME in case the user just entered the word.
- // If the user changed the word in the dialog box, then we want to replace
- // what was entered in the text field.
- if (null == uri || !(context instanceof LatinIME)) return;
- final long changedRowId = ContentUris.parseId(uri);
- if (-1 == changedRowId) return; // Unknown content... Not sure why we're here
- final String changedWord = getChangedWordForUri(uri);
- ((LatinIME)context).onWordAddedToUserDictionary(changedWord);
}
};
cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
@@ -115,19 +113,6 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
loadDictionary();
}
- private String getChangedWordForUri(final Uri uri) {
- final Cursor cursor = mContext.getContentResolver().query(uri,
- PROJECTION_QUERY, null, null, null);
- if (cursor == null) return null;
- try {
- if (!cursor.moveToFirst()) return null;
- final int indexWord = cursor.getColumnIndex(Words.WORD);
- return cursor.getString(indexWord);
- } finally {
- cursor.close();
- }
- }
-
@Override
public synchronized void close() {
if (mObserver != null) {
@@ -186,12 +171,19 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
} else {
requestArguments = localeElements;
}
- final Cursor cursor = mContext.getContentResolver().query(
- Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null);
+ Cursor cursor = null;
try {
+ cursor = mContext.getContentResolver().query(
+ Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null);
addWords(cursor);
+ } catch (final SQLiteException e) {
+ Log.e(TAG, "SQLiteException in the remote User dictionary process.", e);
} finally {
- if (null != cursor) cursor.close();
+ try {
+ if (null != cursor) cursor.close();
+ } catch (final SQLiteException e) {
+ Log.e(TAG, "SQLiteException in the remote User dictionary process.", e);
+ }
}
}
@@ -239,7 +231,6 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
private void addWords(final Cursor cursor) {
final boolean hasShortcutColumn = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
- clearFusionDictionary();
if (cursor == null) return;
if (cursor.moveToFirst()) {
final int indexWord = cursor.getColumnIndex(Words.WORD);
@@ -266,4 +257,9 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
protected boolean hasContentChanged() {
return true;
}
+
+ @Override
+ protected boolean needsToReloadBeforeWriting() {
+ return true;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
deleted file mode 100644
index 528028328..000000000
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
+++ /dev/null
@@ -1,406 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.AsyncTask;
-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.UserHistoryDictIOUtils.BigramDictionaryInterface;
-import com.android.inputmethod.latin.UserHistoryDictIOUtils.OnAddWordListener;
-import com.android.inputmethod.latin.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
-import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.lang.ref.SoftReference;
-import java.util.ArrayList;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * 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 final class UserHistoryDictionary extends ExpandableDictionary {
- private static final String TAG = UserHistoryDictionary.class.getSimpleName();
- private static final String NAME = UserHistoryDictionary.class.getSimpleName();
- public static final boolean DBG_SAVE_RESTORE = false;
- public static final boolean DBG_STRESS_TEST = false;
- public static final boolean DBG_ALWAYS_WRITE = false;
- public static final boolean PROFILE_SAVE_RESTORE = LatinImeLogger.sDBG;
-
- private static final FormatOptions VERSION3 = new FormatOptions(3,
- true /* supportsDynamicUpdate */);
-
- /** Any pair being typed or picked */
- private static final int FREQUENCY_FOR_TYPED = 2;
-
- /** Maximum number of pairs. Pruning will start when databases goes above this number. */
- public static final int MAX_HISTORY_BIGRAMS = 10000;
-
- /**
- * When it hits maximum bigram pair, it will delete until you are left with
- * only (sMaxHistoryBigrams - sDeleteHistoryBigrams) pairs.
- * Do not keep this number small to avoid deleting too often.
- */
- public static final int DELETE_HISTORY_BIGRAMS = 1000;
-
- /** Locale for which this user history dictionary is storing words */
- private final String mLocale;
-
- private final UserHistoryDictionaryBigramList mBigramList =
- new UserHistoryDictionaryBigramList();
- private final ReentrantLock mBigramListLock = new ReentrantLock();
- private final SharedPreferences mPrefs;
-
- // Should always be false except when we use this class for test
- @UsedForTesting boolean isTest = false;
-
- private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>
- sLangDictCache = CollectionUtils.newConcurrentHashMap();
-
- public static synchronized UserHistoryDictionary getInstance(
- final Context context, final String locale, final SharedPreferences sp) {
- if (sLangDictCache.containsKey(locale)) {
- final SoftReference<UserHistoryDictionary> ref = sLangDictCache.get(locale);
- final UserHistoryDictionary dict = ref == null ? null : ref.get();
- if (dict != null) {
- if (PROFILE_SAVE_RESTORE) {
- Log.w(TAG, "Use cached UserHistoryDictionary for " + locale);
- }
- return dict;
- }
- }
- final UserHistoryDictionary dict =
- new UserHistoryDictionary(context, locale, sp);
- sLangDictCache.put(locale, new SoftReference<UserHistoryDictionary>(dict));
- return dict;
- }
-
- private UserHistoryDictionary(final Context context, final String locale,
- final SharedPreferences sp) {
- super(context, Dictionary.TYPE_USER_HISTORY);
- mLocale = locale;
- mPrefs = sp;
- if (mLocale != null && mLocale.length() > 1) {
- loadDictionary();
- }
- }
-
- @Override
- public void close() {
- flushPendingWrites();
- // Don't close the database as locale changes will require it to be reopened anyway
- // Also, the database is written to somewhat frequently, so it needs to be kept alive
- // throughout the life of the process.
- // mOpenHelper.close();
- // Ignore close because we cache UserHistoryDictionary for each language. See getInstance()
- // above.
- // super.close();
- }
-
- @Override
- protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer composer,
- final String prevWord, final ProximityInfo proximityInfo) {
- // Inhibit suggestions (not predictions) for user history for now. Removing this method
- // is enough to use it through the standard ExpandableDictionary way.
- return null;
- }
-
- /**
- * Return whether the passed charsequence is in the dictionary.
- */
- @Override
- public synchronized boolean isValidWord(final String word) {
- // TODO: figure out what is the correct thing to do here.
- return false;
- }
-
- /**
- * Pair will be added to the user history dictionary.
- *
- * The first word may be null. That means we don't know the context, in other words,
- * it's only a unigram. The first word may also be an empty string : this means start
- * context, as in beginning of a sentence for example.
- * The second word may not be null (a NullPointerException would be thrown).
- */
- public int addToUserHistory(final String word1, final String word2, final boolean isValid) {
- if (word2.length() >= Constants.Dictionary.MAX_WORD_LENGTH ||
- (word1 != null && word1.length() >= Constants.Dictionary.MAX_WORD_LENGTH)) {
- return -1;
- }
- if (mBigramListLock.tryLock()) {
- try {
- super.addWord(
- word2, null /* the "shortcut" parameter is null */, FREQUENCY_FOR_TYPED);
- mBigramList.addBigram(null, word2, (byte)FREQUENCY_FOR_TYPED);
- // Do not insert a word as a bigram of itself
- if (word2.equals(word1)) {
- return 0;
- }
- final int freq;
- if (null == word1) {
- freq = FREQUENCY_FOR_TYPED;
- } else {
- freq = super.setBigramAndGetFrequency(
- word1, word2, new ForgettingCurveParams(isValid));
- }
- mBigramList.addBigram(word1, word2);
- return freq;
- } finally {
- mBigramListLock.unlock();
- }
- }
- return -1;
- }
-
- public boolean cancelAddingUserHistory(final String word1, final String word2) {
- if (mBigramListLock.tryLock()) {
- try {
- if (mBigramList.removeBigram(word1, word2)) {
- return super.removeBigram(word1, word2);
- }
- } finally {
- mBigramListLock.unlock();
- }
- }
- return false;
- }
-
- /**
- * Schedules a background thread to write any pending words to the database.
- */
- private void flushPendingWrites() {
- // Create a background thread to write the pending entries
- new UpdateBinaryTask(mBigramList, mLocale, this, mPrefs, getContext()).execute();
- }
-
- @Override
- public void loadDictionaryAsync() {
- // This must be run on non-main thread
- mBigramListLock.lock();
- try {
- loadDictionaryAsyncLocked();
- } finally {
- mBigramListLock.unlock();
- }
- }
-
- private int profTotal;
-
- private void loadDictionaryAsyncLocked() {
- if (DBG_STRESS_TEST) {
- try {
- Log.w(TAG, "Start stress in loading: " + mLocale);
- Thread.sleep(15000);
- Log.w(TAG, "End stress in loading");
- } catch (InterruptedException e) {
- }
- }
- final long last = Settings.readLastUserHistoryWriteTime(mPrefs, mLocale);
- final boolean initializing = last == 0;
- final long now = System.currentTimeMillis();
- profTotal = 0;
- final String fileName = NAME + "." + mLocale + ".dict";
- final ExpandableDictionary dictionary = this;
- final OnAddWordListener listener = new OnAddWordListener() {
- @Override
- public void setUnigram(final String word, final String shortcutTarget,
- final int frequency) {
- profTotal++;
- if (DBG_SAVE_RESTORE) {
- Log.d(TAG, "load unigram: " + word + "," + frequency);
- }
- dictionary.addWord(word, shortcutTarget, frequency);
- mBigramList.addBigram(null, word, (byte)frequency);
- }
-
- @Override
- public void setBigram(final String word1, final String word2, final int frequency) {
- if (word1.length() < Constants.Dictionary.MAX_WORD_LENGTH
- && word2.length() < Constants.Dictionary.MAX_WORD_LENGTH) {
- profTotal++;
- if (DBG_SAVE_RESTORE) {
- Log.d(TAG, "load bigram: " + word1 + "," + word2 + "," + frequency);
- }
- dictionary.setBigramAndGetFrequency(
- word1, word2, initializing ? new ForgettingCurveParams(true)
- : new ForgettingCurveParams(frequency, now, last));
- }
- mBigramList.addBigram(word1, word2, (byte)frequency);
- }
- };
-
- // Load the dictionary from binary file
- FileInputStream inStream = null;
- try {
- final File file = new File(getContext().getFilesDir(), fileName);
- final byte[] buffer = new byte[(int)file.length()];
- inStream = new FileInputStream(file);
- inStream.read(buffer);
- UserHistoryDictIOUtils.readDictionaryBinary(
- new UserHistoryDictIOUtils.ByteArrayWrapper(buffer), listener);
- } catch (FileNotFoundException e) {
- // This is an expected condition: we don't have a user history dictionary for this
- // language yet. It will be created sometime later.
- } catch (IOException e) {
- Log.e(TAG, "IOException on opening a bytebuffer", e);
- } finally {
- if (inStream != null) {
- try {
- inStream.close();
- } catch (IOException e) {
- // do nothing
- }
- }
- if (PROFILE_SAVE_RESTORE) {
- final long diff = System.currentTimeMillis() - now;
- Log.d(TAG, "PROF: Load UserHistoryDictionary: "
- + mLocale + ", " + diff + "ms. load " + profTotal + "entries.");
- }
- }
- }
-
- /**
- * Async task to write pending words to the binarydicts.
- */
- private static final class UpdateBinaryTask extends AsyncTask<Void, Void, Void>
- implements BigramDictionaryInterface {
- private final UserHistoryDictionaryBigramList mBigramList;
- private final boolean mAddLevel0Bigrams;
- private final String mLocale;
- private final UserHistoryDictionary mUserHistoryDictionary;
- private final SharedPreferences mPrefs;
- private final Context mContext;
-
- public UpdateBinaryTask(final UserHistoryDictionaryBigramList pendingWrites,
- final String locale, final UserHistoryDictionary dict,
- final SharedPreferences prefs, final Context context) {
- mBigramList = pendingWrites;
- mLocale = locale;
- mUserHistoryDictionary = dict;
- mPrefs = prefs;
- mContext = context;
- mAddLevel0Bigrams = mBigramList.size() <= MAX_HISTORY_BIGRAMS;
- }
-
- @Override
- protected Void doInBackground(final Void... v) {
- if (mUserHistoryDictionary.isTest) {
- // If isTest == true, wait until the lock is released.
- mUserHistoryDictionary.mBigramListLock.lock();
- try {
- doWriteTaskLocked();
- } finally {
- mUserHistoryDictionary.mBigramListLock.unlock();
- }
- } else if (mUserHistoryDictionary.mBigramListLock.tryLock()) {
- doWriteTaskLocked();
- }
- return null;
- }
-
- private void doWriteTaskLocked() {
- if (DBG_STRESS_TEST) {
- try {
- Log.w(TAG, "Start stress in closing: " + mLocale);
- Thread.sleep(15000);
- Log.w(TAG, "End stress in closing");
- } catch (InterruptedException e) {
- Log.e(TAG, "In stress test", e);
- }
- }
-
- final long now = PROFILE_SAVE_RESTORE ? System.currentTimeMillis() : 0;
- final String fileName = NAME + "." + mLocale + ".dict";
- final File file = new File(mContext.getFilesDir(), fileName);
- FileOutputStream out = null;
-
- try {
- out = new FileOutputStream(file);
- UserHistoryDictIOUtils.writeDictionaryBinary(out, this, mBigramList, VERSION3);
- out.flush();
- out.close();
- } catch (IOException e) {
- Log.e(TAG, "IO Exception while writing file", e);
- } finally {
- if (out != null) {
- try {
- out.close();
- } catch (IOException e) {
- // ignore
- }
- }
- }
-
- // Save the timestamp after we finish writing the binary dictionary.
- Settings.writeLastUserHistoryWriteTime(mPrefs, mLocale);
- if (PROFILE_SAVE_RESTORE) {
- final long diff = System.currentTimeMillis() - now;
- Log.w(TAG, "PROF: Write User HistoryDictionary: " + mLocale + ", " + diff + "ms.");
- }
- }
-
- @Override
- public int getFrequency(final String word1, final String word2) {
- final int freq;
- if (word1 == null) { // unigram
- freq = FREQUENCY_FOR_TYPED;
- final byte prevFc = mBigramList.getBigrams(word1).get(word2);
- } else { // bigram
- final NextWord nw = mUserHistoryDictionary.getBigramWord(word1, word2);
- if (nw != null) {
- final ForgettingCurveParams fcp = nw.getFcParams();
- final byte prevFc = mBigramList.getBigrams(word1).get(word2);
- final byte fc = fcp.getFc();
- final boolean isValid = fcp.isValid();
- if (prevFc > 0 && prevFc == fc) {
- freq = fc & 0xFF;
- } else if (UserHistoryForgettingCurveUtils.
- needsToSave(fc, isValid, mAddLevel0Bigrams)) {
- freq = fc & 0xFF;
- } else {
- // Delete this entry
- freq = -1;
- }
- } else {
- // Delete this entry
- freq = -1;
- }
- }
- return freq;
- }
- }
-
- @UsedForTesting
- void forceAddWordForTest(final String word1, final String word2, final boolean isValid) {
- mBigramListLock.lock();
- try {
- addToUserHistory(word1, word2, isValid);
- } finally {
- mBigramListLock.unlock();
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
deleted file mode 100644
index 949720fda..000000000
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ /dev/null
@@ -1,511 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.app.Activity;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.inputmethodservice.InputMethodService;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Environment;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.channels.FileChannel;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-
-public final class Utils {
- private static final String TAG = Utils.class.getSimpleName();
-
- private Utils() {
- // This utility class is not publicly instantiable.
- }
-
- /**
- * Cancel an {@link AsyncTask}.
- *
- * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
- * task should be interrupted; otherwise, in-progress tasks are allowed
- * to complete.
- */
- public static void cancelTask(final AsyncTask<?, ?, ?> task,
- final boolean mayInterruptIfRunning) {
- if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
- task.cancel(mayInterruptIfRunning);
- }
- }
-
- /* package */ static final class RingCharBuffer {
- private static RingCharBuffer sRingCharBuffer = new RingCharBuffer();
- private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
- private static final int INVALID_COORDINATE = -2;
- /* package */ static final int BUFSIZE = 20;
- private InputMethodService mContext;
- private boolean mEnabled = false;
- private int mEnd = 0;
- /* package */ int mLength = 0;
- private char[] mCharBuf = new char[BUFSIZE];
- private int[] mXBuf = new int[BUFSIZE];
- private int[] mYBuf = new int[BUFSIZE];
-
- private RingCharBuffer() {
- // Intentional empty constructor for singleton.
- }
-
- @UsedForTesting
- public static RingCharBuffer getInstance() {
- return sRingCharBuffer;
- }
-
- public static RingCharBuffer init(final InputMethodService context, final boolean enabled,
- final boolean usabilityStudy) {
- if (!(enabled || usabilityStudy)) {
- return null;
- }
- sRingCharBuffer.mContext = context;
- sRingCharBuffer.mEnabled = true;
- UsabilityStudyLogUtils.getInstance().init(context);
- return sRingCharBuffer;
- }
-
- private static int normalize(final int in) {
- int ret = in % BUFSIZE;
- return ret < 0 ? ret + BUFSIZE : ret;
- }
-
- // TODO: accept code points
- @UsedForTesting
- public void push(final char c, final int x, final int y) {
- if (!mEnabled) {
- return;
- }
- mCharBuf[mEnd] = c;
- mXBuf[mEnd] = x;
- mYBuf[mEnd] = y;
- mEnd = normalize(mEnd + 1);
- if (mLength < BUFSIZE) {
- ++mLength;
- }
- }
-
- public char pop() {
- if (mLength < 1) {
- return PLACEHOLDER_DELIMITER_CHAR;
- }
- mEnd = normalize(mEnd - 1);
- --mLength;
- return mCharBuf[mEnd];
- }
-
- public char getBackwardNthChar(final int n) {
- if (mLength <= n || n < 0) {
- return PLACEHOLDER_DELIMITER_CHAR;
- }
- return mCharBuf[normalize(mEnd - n - 1)];
- }
-
- public int getPreviousX(final char c, final int back) {
- final int index = normalize(mEnd - 2 - back);
- if (mLength <= back
- || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
- return INVALID_COORDINATE;
- }
- return mXBuf[index];
- }
-
- public int getPreviousY(final char c, final int back) {
- int index = normalize(mEnd - 2 - back);
- if (mLength <= back
- || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
- return INVALID_COORDINATE;
- }
- return mYBuf[index];
- }
-
- public String getLastWord(final int ignoreCharCount) {
- final StringBuilder sb = new StringBuilder();
- final LatinIME latinIme = (LatinIME)mContext;
- int i = ignoreCharCount;
- for (; i < mLength; ++i) {
- final char c = mCharBuf[normalize(mEnd - 1 - i)];
- if (!latinIme.isWordSeparator(c)) {
- break;
- }
- }
- for (; i < mLength; ++i) {
- char c = mCharBuf[normalize(mEnd - 1 - i)];
- if (!latinIme.isWordSeparator(c)) {
- sb.append(c);
- } else {
- break;
- }
- }
- return sb.reverse().toString();
- }
-
- public void reset() {
- mLength = 0;
- }
- }
-
- // Get the current stack trace
- public static String getStackTrace(final int limit) {
- final StringBuilder sb = new StringBuilder();
- try {
- throw new RuntimeException();
- } catch (final RuntimeException e) {
- final StackTraceElement[] frames = e.getStackTrace();
- // Start at 1 because the first frame is here and we don't care about it
- for (int j = 1; j < frames.length && j < limit + 1; ++j) {
- sb.append(frames[j].toString() + "\n");
- }
- }
- return sb.toString();
- }
-
- public static String getStackTrace() {
- return getStackTrace(Integer.MAX_VALUE - 1);
- }
-
- public static final class UsabilityStudyLogUtils {
- // TODO: remove code duplication with ResearchLog class
- private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
- private static final String FILENAME = "log.txt";
- private final Handler mLoggingHandler;
- private File mFile;
- private File mDirectory;
- private InputMethodService mIms;
- private PrintWriter mWriter;
- private final Date mDate;
- private final SimpleDateFormat mDateFormat;
-
- private UsabilityStudyLogUtils() {
- mDate = new Date();
- mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ", Locale.US);
-
- HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task",
- Process.THREAD_PRIORITY_BACKGROUND);
- handlerThread.start();
- mLoggingHandler = new Handler(handlerThread.getLooper());
- }
-
- // Initialization-on-demand holder
- private static final class OnDemandInitializationHolder {
- public static final UsabilityStudyLogUtils sInstance = new UsabilityStudyLogUtils();
- }
-
- public static UsabilityStudyLogUtils getInstance() {
- return OnDemandInitializationHolder.sInstance;
- }
-
- public void init(final InputMethodService ims) {
- mIms = ims;
- mDirectory = ims.getFilesDir();
- }
-
- private void createLogFileIfNotExist() {
- if ((mFile == null || !mFile.exists())
- && (mDirectory != null && mDirectory.exists())) {
- try {
- mWriter = getPrintWriter(mDirectory, FILENAME, false);
- } catch (final IOException e) {
- Log.e(USABILITY_TAG, "Can't create log file.");
- }
- }
- }
-
- public static void writeBackSpace(final int x, final int y) {
- UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y);
- }
-
- public static void writeChar(final char c, final int x, final int y) {
- String inputChar = String.valueOf(c);
- switch (c) {
- case '\n':
- inputChar = "<enter>";
- break;
- case '\t':
- inputChar = "<tab>";
- break;
- case ' ':
- inputChar = "<space>";
- break;
- }
- UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y);
- LatinImeLogger.onPrintAllUsabilityStudyLogs();
- }
-
- public void write(final String log) {
- mLoggingHandler.post(new Runnable() {
- @Override
- public void run() {
- createLogFileIfNotExist();
- final long currentTime = System.currentTimeMillis();
- mDate.setTime(currentTime);
-
- final String printString = String.format(Locale.US, "%s\t%d\t%s\n",
- mDateFormat.format(mDate), currentTime, log);
- if (LatinImeLogger.sDBG) {
- Log.d(USABILITY_TAG, "Write: " + log);
- }
- mWriter.print(printString);
- }
- });
- }
-
- private synchronized String getBufferedLogs() {
- mWriter.flush();
- final StringBuilder sb = new StringBuilder();
- final BufferedReader br = getBufferedReader();
- String line;
- try {
- while ((line = br.readLine()) != null) {
- sb.append('\n');
- sb.append(line);
- }
- } catch (final IOException e) {
- Log.e(USABILITY_TAG, "Can't read log file.");
- } finally {
- if (LatinImeLogger.sDBG) {
- Log.d(USABILITY_TAG, "Got all buffered logs\n" + sb.toString());
- }
- try {
- br.close();
- } catch (final IOException e) {
- // ignore.
- }
- }
- return sb.toString();
- }
-
- public void emailResearcherLogsAll() {
- mLoggingHandler.post(new Runnable() {
- @Override
- public void run() {
- final Date date = new Date();
- date.setTime(System.currentTimeMillis());
- final String currentDateTimeString =
- new SimpleDateFormat("yyyyMMdd-HHmmssZ", Locale.US).format(date);
- if (mFile == null) {
- Log.w(USABILITY_TAG, "No internal log file found.");
- return;
- }
- if (mIms.checkCallingOrSelfPermission(
- android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
- != PackageManager.PERMISSION_GRANTED) {
- Log.w(USABILITY_TAG, "Doesn't have the permission WRITE_EXTERNAL_STORAGE");
- return;
- }
- mWriter.flush();
- final String destPath = Environment.getExternalStorageDirectory()
- + "/research-" + currentDateTimeString + ".log";
- final File destFile = new File(destPath);
- try {
- final FileInputStream srcStream = new FileInputStream(mFile);
- final FileOutputStream destStream = new FileOutputStream(destFile);
- final FileChannel src = srcStream.getChannel();
- final FileChannel dest = destStream.getChannel();
- src.transferTo(0, src.size(), dest);
- src.close();
- srcStream.close();
- dest.close();
- destStream.close();
- } catch (final FileNotFoundException e1) {
- Log.w(USABILITY_TAG, e1);
- return;
- } catch (final IOException e2) {
- Log.w(USABILITY_TAG, e2);
- return;
- }
- if (destFile == null || !destFile.exists()) {
- Log.w(USABILITY_TAG, "Dest file doesn't exist.");
- return;
- }
- final Intent intent = new Intent(Intent.ACTION_SEND);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- if (LatinImeLogger.sDBG) {
- Log.d(USABILITY_TAG, "Destination file URI is " + destFile.toURI());
- }
- intent.setType("text/plain");
- intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + destPath));
- intent.putExtra(Intent.EXTRA_SUBJECT,
- "[Research Logs] " + currentDateTimeString);
- mIms.startActivity(intent);
- }
- });
- }
-
- public void printAll() {
- mLoggingHandler.post(new Runnable() {
- @Override
- public void run() {
- mIms.getCurrentInputConnection().commitText(getBufferedLogs(), 0);
- }
- });
- }
-
- public void clearAll() {
- mLoggingHandler.post(new Runnable() {
- @Override
- public void run() {
- if (mFile != null && mFile.exists()) {
- if (LatinImeLogger.sDBG) {
- Log.d(USABILITY_TAG, "Delete log file.");
- }
- mFile.delete();
- mWriter.close();
- }
- }
- });
- }
-
- private BufferedReader getBufferedReader() {
- createLogFileIfNotExist();
- try {
- return new BufferedReader(new FileReader(mFile));
- } catch (final FileNotFoundException e) {
- return null;
- }
- }
-
- private PrintWriter getPrintWriter(final File dir, final String filename,
- final boolean renew) throws IOException {
- mFile = new File(dir, filename);
- if (mFile.exists()) {
- if (renew) {
- mFile.delete();
- }
- }
- return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */);
- }
- }
-
- public static final class Stats {
- public static void onNonSeparator(final char code, final int x, final int y) {
- RingCharBuffer.getInstance().push(code, x, y);
- LatinImeLogger.logOnInputChar();
- }
-
- 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);
- }
-
- public static void onSeparator(final String separator, final int x, final int y) {
- final int length = separator.length();
- for (int i = 0; i < length; i = Character.offsetByCodePoints(separator, i, 1)) {
- int codePoint = Character.codePointAt(separator, i);
- // TODO: accept code points
- RingCharBuffer.getInstance().push((char)codePoint, x, y);
- }
- LatinImeLogger.logOnInputSeparator();
- }
-
- public static void onAutoCorrection(final String typedWord, final String correctedWord,
- final String separatorString, final WordComposer wordComposer) {
- final boolean isBatchMode = wordComposer.isBatchMode();
- if (!isBatchMode && TextUtils.isEmpty(typedWord)) {
- return;
- }
- // TODO: this fails when the separator is more than 1 code point long, but
- // the backend can't handle it yet. The only case when this happens is with
- // smileys and other multi-character keys.
- final int codePoint = TextUtils.isEmpty(separatorString) ? Constants.NOT_A_CODE
- : separatorString.codePointAt(0);
- if (!isBatchMode) {
- LatinImeLogger.logOnAutoCorrectionForTyping(typedWord, correctedWord, codePoint);
- } else {
- if (!TextUtils.isEmpty(correctedWord)) {
- // We must make sure that InputPointer contains only the relative timestamps,
- // not actual timestamps.
- LatinImeLogger.logOnAutoCorrectionForGeometric(
- "", correctedWord, codePoint, wordComposer.getInputPointers());
- }
- }
- }
-
- public static void onAutoCorrectionCancellation() {
- LatinImeLogger.logOnAutoCorrectionCancelled();
- }
- }
-
- public static String getDebugInfo(final SuggestedWords suggestions, final int pos) {
- if (!LatinImeLogger.sDBG) {
- return null;
- }
- final SuggestedWordInfo wordInfo = suggestions.getInfo(pos);
- if (wordInfo == null) {
- return null;
- }
- final String info = wordInfo.getDebugString();
- if (TextUtils.isEmpty(info)) {
- return null;
- }
- return info;
- }
-
- public static int getAcitivityTitleResId(final Context context,
- final Class<? extends Activity> cls) {
- final ComponentName cn = new ComponentName(context, cls);
- try {
- final ActivityInfo ai = context.getPackageManager().getActivityInfo(cn, 0);
- if (ai != null) {
- return ai.labelRes;
- }
- } catch (final NameNotFoundException e) {
- Log.e(TAG, "Failed to get settings activity title res id.", e);
- }
- return 0;
- }
-
- public static String getVersionName(final Context context) {
- try {
- if (context == null) {
- return "";
- }
- final String packageName = context.getPackageName();
- final PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
- return info.versionName;
- } catch (final NameNotFoundException e) {
- Log.e(TAG, "Could not find version info.", e);
- }
- return "";
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index e078f03f4..039dadc66 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -18,6 +18,7 @@ package com.android.inputmethod.latin;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.utils.StringUtils;
import java.util.Arrays;
@@ -25,7 +26,7 @@ import java.util.Arrays;
* A place to store the currently composing word with information such as adjacent key codes as well
*/
public final class WordComposer {
- private static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH;
+ private static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
private static final boolean DBG = LatinImeLogger.sDBG;
public static final int CAPS_MODE_OFF = 0;
@@ -36,8 +37,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 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;
private String mAutoCorrection;
private boolean mIsResumed;
@@ -55,6 +64,10 @@ public final class WordComposer {
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
+ // code points.
private int mCodePointSize;
private int mCursorPositionWithinWord;
@@ -192,6 +205,49 @@ public final class WordComposer {
return mCursorPositionWithinWord != mCodePointSize;
}
+ /**
+ * When the cursor is moved by the user, we need to update its position.
+ * If it falls inside the currently composing word, we don't reset the composition, and
+ * only update the cursor position.
+ *
+ * @param expectedMoveAmount How many java chars to move the cursor. Negative values move
+ * the cursor backward, positive values move the cursor forward.
+ * @return true if the cursor is still inside the composing word, false otherwise.
+ */
+ public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) {
+ 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;
+ }
+ if (expectedMoveAmount >= 0) {
+ // Moving the cursor forward for the expected amount or until the end of the word has
+ // been reached, whichever comes first.
+ while (actualMoveAmountWithinWord < expectedMoveAmount && cursorPos < mCodePointSize) {
+ actualMoveAmountWithinWord += Character.charCount(codePoints[cursorPos]);
+ ++cursorPos;
+ }
+ } else {
+ // Moving the cursor backward for the expected amount or until the start of the word
+ // has been reached, whichever comes first.
+ while (actualMoveAmountWithinWord > expectedMoveAmount && cursorPos > 0) {
+ --cursorPos;
+ actualMoveAmountWithinWord -= Character.charCount(codePoints[cursorPos]);
+ }
+ }
+ // If the actual and expected amounts differ, we crossed the start or the end of the word
+ // so the result would not be inside the composing word.
+ if (actualMoveAmountWithinWord != expectedMoveAmount) return false;
+ mCursorPositionWithinWord = cursorPos;
+ return true;
+ }
+
public void setBatchInputPointers(final InputPointers batchPointers) {
mInputPointers.set(batchPointers);
mIsBatchMode = true;
@@ -216,8 +272,8 @@ public final class WordComposer {
final int x, y;
final Key key;
if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) {
- x = key.mX + key.mWidth / 2;
- y = key.mY + key.mHeight / 2;
+ x = key.getX() + key.getWidth() / 2;
+ y = key.getY() + key.getHeight() / 2;
} else {
x = Constants.NOT_A_COORDINATE;
y = Constants.NOT_A_COORDINATE;
diff --git a/java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java
index 9f91639a2..028f78a87 100644
--- a/java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java
+++ b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java
@@ -14,15 +14,22 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.debug;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
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.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
+import com.android.inputmethod.latin.utils.LocaleUtils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -56,7 +63,7 @@ public class ExternalDictionaryGetterForDebug {
if (0 == fileNames.length) {
showNoFileDialog(context);
} else if (1 == fileNames.length) {
- askInstallFile(context, fileNames[0]);
+ askInstallFile(context, SOURCE_FOLDER, fileNames[0], null /* completeRunnable */);
} else {
showChooseFileDialog(context, fileNames);
}
@@ -79,14 +86,19 @@ public class ExternalDictionaryGetterForDebug {
.setItems(fileNames, new OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
- askInstallFile(context, fileNames[which]);
+ askInstallFile(context, SOURCE_FOLDER, fileNames[which],
+ null /* completeRunnable */);
}
})
.create().show();
}
- private static void askInstallFile(final Context context, final String fileName) {
- final File file = new File(SOURCE_FOLDER, fileName.toString());
+ /**
+ * Shows a dialog which offers the user to install the external dictionary.
+ */
+ 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 StringBuilder message = new StringBuilder();
final String locale = header.getLocaleString();
@@ -106,12 +118,26 @@ public class ExternalDictionaryGetterForDebug {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
+ if (completeRunnable != null) {
+ completeRunnable.run();
+ }
}
}).setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
installFile(context, file, header);
dialog.dismiss();
+ if (completeRunnable != null) {
+ completeRunnable.run();
+ }
+ }
+ }).setOnCancelListener(new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ // Canceled by the user by hitting the back key
+ if (completeRunnable != null) {
+ completeRunnable.run();
+ }
}
}).create().show();
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
new file mode 100644
index 000000000..665c7a27c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
@@ -0,0 +1,624 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+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 a ByteArrayOutputStream.
+ *
+ * This will also write the terminator byte.
+ *
+ * @param buffer the ByteArrayOutputStream to write to.
+ * @param word the string to write.
+ */
+ static void writeString(final ByteArrayOutputStream buffer, final String word) {
+ 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;
+ }
+ }
+ int 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
new file mode 100644
index 000000000..6cc0bfb76
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
@@ -0,0 +1,958 @@
+/*
+ * 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;
+ final int nodeSize = branches.size();
+ 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));
+ }
+ 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;
+ }
+ final int nodeSize = nodeCountSize + nodeffset
+ + (formatOptions.mSupportsDynamicUpdate
+ ? FormatSpec.FORWARD_LINK_ADDRESS_SIZE : 0);
+ 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 ptNodeAddress,
+ 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
index c87a9254d..a282f595c 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
@@ -18,56 +18,52 @@ package com.android.inputmethod.latin.makedict;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.CharEncoding;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
+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.CharGroup;
+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.nio.channels.FileChannel;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;
public final class BinaryDictIOUtils {
private static final boolean DBG = false;
- private static final int MSB24 = 0x800000;
- private static final int SINT24_MAX = 0x7FFFFF;
- private static final int MAX_JUMPS = 10000;
private BinaryDictIOUtils() {
// This utility class is not publicly instantiable.
}
private static final class Position {
- public static final int NOT_READ_GROUPCOUNT = -1;
+ public static final int NOT_READ_PTNODE_COUNT = -1;
public int mAddress;
- public int mNumOfCharGroup;
+ public int mNumOfPtNode;
public int mPosition;
public int mLength;
public Position(int address, int length) {
mAddress = address;
mLength = length;
- mNumOfCharGroup = NOT_READ_GROUPCOUNT;
+ mNumOfPtNode = NOT_READ_PTNODE_COUNT;
}
}
/**
- * Tours all node without recursive call.
+ * Retrieves all node arrays without recursive call.
*/
- private static void readUnigramsAndBigramsBinaryInner(
- final FusionDictionaryBufferInterface buffer, final int headerSize,
- final Map<Integer, String> words, final Map<Integer, Integer> frequencies,
+ 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];
@@ -82,47 +78,47 @@ public final class BinaryDictIOUtils {
Position p = stack.peek();
if (DBG) {
- MakedictLog.d("read: address=" + p.mAddress + ", numOfCharGroup=" +
- p.mNumOfCharGroup + ", position=" + p.mPosition + ", length=" + p.mLength);
+ MakedictLog.d("read: address=" + p.mAddress + ", numOfPtNode=" +
+ p.mNumOfPtNode + ", position=" + p.mPosition + ", length=" + p.mLength);
}
- if (buffer.position() != p.mAddress) buffer.position(p.mAddress);
+ if (dictDecoder.getPosition() != p.mAddress) dictDecoder.setPosition(p.mAddress);
if (index != p.mLength) index = p.mLength;
- if (p.mNumOfCharGroup == Position.NOT_READ_GROUPCOUNT) {
- p.mNumOfCharGroup = BinaryDictInputOutput.readCharGroupCount(buffer);
- p.mAddress += BinaryDictInputOutput.getGroupCountSize(p.mNumOfCharGroup);
+ if (p.mNumOfPtNode == Position.NOT_READ_PTNODE_COUNT) {
+ p.mNumOfPtNode = dictDecoder.readPtNodeCount();
+ p.mAddress += getPtNodeCountSize(p.mNumOfPtNode);
p.mPosition = 0;
}
- if (p.mNumOfCharGroup == 0) {
+ if (p.mNumOfPtNode == 0) {
stack.pop();
continue;
}
- CharGroupInfo info = BinaryDictInputOutput.readCharGroup(buffer,
- p.mAddress - headerSize, formatOptions);
+ 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 isMovedGroup = BinaryDictInputOutput.isMovedGroup(info.mFlags,
+ final boolean isMovedPtNode = isMovedPtNode(info.mFlags,
formatOptions);
- final boolean isDeletedGroup = BinaryDictInputOutput.isDeletedGroup(info.mFlags,
+ final boolean isDeletedPtNode = isDeletedPtNode(info.mFlags,
formatOptions);
- if (!isMovedGroup && !isDeletedGroup
- && info.mFrequency != FusionDictionary.CharGroup.NOT_A_TERMINAL) {// found word
+ 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.mNumOfCharGroup) {
+ if (p.mPosition == p.mNumOfPtNode) {
if (formatOptions.mSupportsDynamicUpdate) {
- final int forwardLinkAddress = buffer.readUnsignedInt24();
- if (forwardLinkAddress != FormatSpec.NO_FORWARD_LINK_ADDRESS) {
- // the node has a forward link.
- p.mNumOfCharGroup = Position.NOT_READ_GROUPCOUNT;
- p.mAddress = forwardLinkAddress;
+ 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();
}
@@ -130,12 +126,12 @@ public final class BinaryDictIOUtils {
stack.pop();
}
} else {
- // the node has more groups.
- p.mAddress = buffer.position();
+ // The Ptnode array has more PtNodes.
+ p.mAddress = dictDecoder.getPosition();
}
- if (!isMovedGroup && BinaryDictInputOutput.hasChildrenAddress(info.mChildrenAddress)) {
- Position childrenPos = new Position(info.mChildrenAddress + headerSize, index);
+ if (!isMovedPtNode && hasChildrenAddress(info.mChildrenAddress)) {
+ final Position childrenPos = new Position(info.mChildrenAddress, index);
stack.push(childrenPos);
}
}
@@ -143,61 +139,59 @@ public final class BinaryDictIOUtils {
/**
* Reads unigrams and bigrams from the binary file.
- * Doesn't make the memory representation of the dictionary.
+ * Doesn't store a full memory representation of the dictionary.
*
- * @param buffer the buffer to read.
+ * @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
- * @throws UnsupportedFormatException
+ * @throws IOException if the file can't be read.
+ * @throws UnsupportedFormatException if the format of the file is not recognized.
*/
- public static void readUnigramsAndBigramsBinary(final FusionDictionaryBufferInterface buffer,
+ /* 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 = BinaryDictInputOutput.readHeader(buffer);
- readUnigramsAndBigramsBinaryInner(buffer, header.mHeaderSize, words, frequencies, bigrams,
- header.mFormatOptions);
+ final FileHeader header = dictDecoder.readHeader();
+ readUnigramsAndBigramsBinaryInner(dictDecoder, header.mHeaderSize, words,
+ frequencies, bigrams, header.mFormatOptions);
}
/**
- * Gets the address of the last CharGroup of the exact matching word in the dictionary.
+ * 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 buffer the buffer to read.
+ * @param dictDecoder the dict decoder.
* @param word the word we search for.
* @return the address of the terminal node.
- * @throws IOException
- * @throws UnsupportedFormatException
+ * @throws IOException if the file can't be read.
+ * @throws UnsupportedFormatException if the format of the file is not recognized.
*/
@UsedForTesting
- public static int getTerminalPosition(final FusionDictionaryBufferInterface buffer,
+ /* package */ static int getTerminalPosition(final DictDecoder dictDecoder,
final String word) throws IOException, UnsupportedFormatException {
if (word == null) return FormatSpec.NOT_VALID_WORD;
- if (buffer.position() != 0) buffer.position(0);
+ dictDecoder.setPosition(0);
- final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
+ 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) {
+ for (int depth = 0; depth < Constants.DICTIONARY_MAX_WORD_LENGTH; ++depth) {
if (wordPos >= wordLen) return FormatSpec.NOT_VALID_WORD;
do {
- final int charGroupCount = BinaryDictInputOutput.readCharGroupCount(buffer);
- boolean foundNextCharGroup = false;
- for (int i = 0; i < charGroupCount; ++i) {
- final int charGroupPos = buffer.position();
- final CharGroupInfo currentInfo = BinaryDictInputOutput.readCharGroup(buffer,
- buffer.position(), header.mFormatOptions);
- final boolean isMovedGroup =
- BinaryDictInputOutput.isMovedGroup(currentInfo.mFlags,
- header.mFormatOptions);
- final boolean isDeletedGroup =
- BinaryDictInputOutput.isDeletedGroup(currentInfo.mFlags,
- header.mFormatOptions);
- if (isMovedGroup) continue;
+ 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;
@@ -210,86 +204,60 @@ public final class BinaryDictIOUtils {
}
if (same) {
- // found the group matches the word.
+ // found the PtNode matches the word.
if (wordPos + currentInfo.mCharacters.length == wordLen) {
- if (currentInfo.mFrequency == CharGroup.NOT_A_TERMINAL
- || isDeletedGroup) {
+ if (currentInfo.mFrequency == PtNode.NOT_A_TERMINAL
+ || isDeletedNode) {
return FormatSpec.NOT_VALID_WORD;
} else {
- return charGroupPos;
+ return ptNodePos;
}
}
wordPos += currentInfo.mCharacters.length;
if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) {
return FormatSpec.NOT_VALID_WORD;
}
- foundNextCharGroup = true;
- buffer.position(currentInfo.mChildrenAddress);
+ foundNextPtNode = true;
+ dictDecoder.setPosition(currentInfo.mChildrenAddress);
break;
}
}
- // If we found the next char group, it is under the file pointer.
- // But if not, we are at the end of this node so we expect to have
+ // 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 in the linked list.
- if (foundNextCharGroup) break;
+ // search on the next node array in the linked list.
+ if (foundNextPtNode) break;
if (!header.mFormatOptions.mSupportsDynamicUpdate) {
return FormatSpec.NOT_VALID_WORD;
}
- final int forwardLinkAddress = buffer.readUnsignedInt24();
- if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
+ final boolean hasValidForwardLinkAddress =
+ dictDecoder.readAndFollowForwardLink();
+ if (!hasValidForwardLinkAddress || !dictDecoder.hasNextPtNodeArray()) {
return FormatSpec.NOT_VALID_WORD;
}
- buffer.position(forwardLinkAddress);
} while(true);
}
return FormatSpec.NOT_VALID_WORD;
}
- private static int markAsDeleted(final int flags) {
- return (flags & (~FormatSpec.MASK_GROUP_ADDRESS_TYPE)) | FormatSpec.FLAG_IS_DELETED;
- }
-
- /**
- * Delete the word from the binary file.
- *
- * @param buffer the buffer to write.
- * @param word the word we delete
- * @throws IOException
- * @throws UnsupportedFormatException
- */
- @UsedForTesting
- public static void deleteWord(final FusionDictionaryBufferInterface buffer,
- final String word) throws IOException, UnsupportedFormatException {
- buffer.position(0);
- final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
- final int wordPosition = getTerminalPosition(buffer, word);
- if (wordPosition == FormatSpec.NOT_VALID_WORD) return;
-
- buffer.position(wordPosition);
- final int flags = buffer.readUnsignedByte();
- buffer.position(wordPosition);
- buffer.put((byte)markAsDeleted(flags));
- }
-
/**
* @return the size written, in bytes. Always 3 bytes.
*/
- private static int writeSInt24ToBuffer(final FusionDictionaryBufferInterface buffer,
+ static int writeSInt24ToBuffer(final DictBuffer dictBuffer,
final int value) {
final int absValue = Math.abs(value);
- buffer.put((byte)(((value < 0 ? 0x80 : 0) | (absValue >> 16)) & 0xFF));
- buffer.put((byte)((absValue >> 8) & 0xFF));
- buffer.put((byte)(absValue & 0xFF));
+ 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.
*/
- private static int writeSInt24ToStream(final OutputStream destination, final int value)
+ 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));
@@ -303,7 +271,7 @@ public final class BinaryDictIOUtils {
*/
private static int writeVariableAddress(final OutputStream destination, final int value)
throws IOException {
- switch (BinaryDictInputOutput.getByteSize(value)) {
+ switch (BinaryDictEncoderUtils.getByteSize(value)) {
case 1:
destination.write((byte)value);
break;
@@ -317,111 +285,52 @@ public final class BinaryDictIOUtils {
destination.write((byte)(0xFF & value));
break;
}
- return BinaryDictInputOutput.getByteSize(value);
- }
-
- /**
- * Update a parent address in a CharGroup that is referred to by groupOriginAddress.
- *
- * @param buffer the buffer to write.
- * @param groupOriginAddress the address of the group.
- * @param newParentAddress the absolute address of the parent.
- * @param formatOptions file format options.
- */
- public static void updateParentAddress(final FusionDictionaryBufferInterface buffer,
- final int groupOriginAddress, final int newParentAddress,
- final FormatOptions formatOptions) {
- final int originalPosition = buffer.position();
- buffer.position(groupOriginAddress);
- if (!formatOptions.mSupportsDynamicUpdate) {
- throw new RuntimeException("this file format does not support parent addresses");
- }
- final int flags = buffer.readUnsignedByte();
- if (BinaryDictInputOutput.isMovedGroup(flags, formatOptions)) {
- // if the group is moved, the parent address is stored in the destination group.
- // We are guaranteed to process the destination group later, so there is no need to
- // update anything here.
- buffer.position(originalPosition);
- return;
- }
- if (DBG) {
- MakedictLog.d("update parent address flags=" + flags + ", " + groupOriginAddress);
- }
- final int parentOffset = newParentAddress - groupOriginAddress;
- writeSInt24ToBuffer(buffer, parentOffset);
- buffer.position(originalPosition);
+ return BinaryDictEncoderUtils.getByteSize(value);
}
- private static void skipCharGroup(final FusionDictionaryBufferInterface buffer,
- final FormatOptions formatOptions) {
- final int flags = buffer.readUnsignedByte();
- BinaryDictInputOutput.readParentAddress(buffer, formatOptions);
- skipString(buffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
- BinaryDictInputOutput.readChildrenAddress(buffer, flags, formatOptions);
- if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) buffer.readUnsignedByte();
+ static void skipPtNode(final DictBuffer dictBuffer, final FormatOptions formatOptions) {
+ final int flags = dictBuffer.readUnsignedByte();
+ BinaryDictDecoderUtils.readParentAddress(dictBuffer, formatOptions);
+ skipString(dictBuffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
+ BinaryDictDecoderUtils.readChildrenAddress(dictBuffer, flags, formatOptions);
+ if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) dictBuffer.readUnsignedByte();
if ((flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS) != 0) {
- final int shortcutsSize = buffer.readUnsignedShort();
- buffer.position(buffer.position() + shortcutsSize
- - FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE);
+ final int shortcutsSize = dictBuffer.readUnsignedShort();
+ dictBuffer.position(dictBuffer.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_GROUP) {
- final int bigramFlags = buffer.readUnsignedByte();
- switch (bigramFlags & FormatSpec.MASK_ATTRIBUTE_ADDRESS_TYPE) {
- case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
- buffer.readUnsignedByte();
+ while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
+ final int bigramFlags = dictBuffer.readUnsignedByte();
+ switch (bigramFlags & FormatSpec.MASK_BIGRAM_ATTR_ADDRESS_TYPE) {
+ case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE:
+ dictBuffer.readUnsignedByte();
break;
- case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
- buffer.readUnsignedShort();
+ case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES:
+ dictBuffer.readUnsignedShort();
break;
- case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
- buffer.readUnsignedInt24();
+ case FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES:
+ dictBuffer.readUnsignedInt24();
break;
}
- if ((bigramFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT) == 0) break;
+ if ((bigramFlags & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT) == 0) break;
}
- if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_GROUP) {
- throw new RuntimeException("Too many bigrams in a group.");
+ if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_PTNODE) {
+ throw new RuntimeException("Too many bigrams in a PtNode.");
}
}
}
- /**
- * Update parent addresses in a Node that is referred to by nodeOriginAddress.
- *
- * @param buffer the buffer to be modified.
- * @param nodeOriginAddress the address of a modified Node.
- * @param newParentAddress the address to be written.
- * @param formatOptions file format options.
- */
- public static void updateParentAddresses(final FusionDictionaryBufferInterface buffer,
- final int nodeOriginAddress, final int newParentAddress,
- final FormatOptions formatOptions) {
- final int originalPosition = buffer.position();
- buffer.position(nodeOriginAddress);
- do {
- final int count = BinaryDictInputOutput.readCharGroupCount(buffer);
- for (int i = 0; i < count; ++i) {
- updateParentAddress(buffer, buffer.position(), newParentAddress, formatOptions);
- skipCharGroup(buffer, formatOptions);
- }
- final int forwardLinkAddress = buffer.readUnsignedInt24();
- buffer.position(forwardLinkAddress);
- } while (formatOptions.mSupportsDynamicUpdate
- && buffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
- buffer.position(originalPosition);
- }
-
- private static void skipString(final FusionDictionaryBufferInterface buffer,
+ static void skipString(final DictBuffer dictBuffer,
final boolean hasMultipleChars) {
if (hasMultipleChars) {
- int character = CharEncoding.readChar(buffer);
+ int character = CharEncoding.readChar(dictBuffer);
while (character != FormatSpec.INVALID_CHARACTER) {
- character = CharEncoding.readChar(buffer);
+ character = CharEncoding.readChar(dictBuffer);
}
} else {
- CharEncoding.readChar(buffer);
+ CharEncoding.readChar(dictBuffer);
}
}
@@ -449,46 +358,24 @@ public final class BinaryDictIOUtils {
size += 3;
}
}
- destination.write((byte)FormatSpec.GROUP_CHARACTERS_TERMINATOR);
- size += FormatSpec.GROUP_TERMINATOR_SIZE;
+ destination.write((byte)FormatSpec.PTNODE_CHARACTERS_TERMINATOR);
+ size += FormatSpec.PTNODE_TERMINATOR_SIZE;
return size;
}
/**
- * Update a children address in a CharGroup that is addressed by groupOriginAddress.
- *
- * @param buffer the buffer to write.
- * @param groupOriginAddress the address of the group.
- * @param newChildrenAddress the absolute address of the child.
- * @param formatOptions file format options.
- */
- public static void updateChildrenAddress(final FusionDictionaryBufferInterface buffer,
- final int groupOriginAddress, final int newChildrenAddress,
- final FormatOptions formatOptions) {
- final int originalPosition = buffer.position();
- buffer.position(groupOriginAddress);
- final int flags = buffer.readUnsignedByte();
- final int parentAddress = BinaryDictInputOutput.readParentAddress(buffer, formatOptions);
- skipString(buffer, (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS) != 0);
- if ((flags & FormatSpec.FLAG_IS_TERMINAL) != 0) buffer.readUnsignedByte();
- final int childrenOffset = newChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS
- ? FormatSpec.NO_CHILDREN_ADDRESS : newChildrenAddress - buffer.position();
- writeSInt24ToBuffer(buffer, childrenOffset);
- buffer.position(originalPosition);
- }
-
- /**
- * Write a char group to an output stream.
- * A char group is an in-memory representation of a node in trie.
- * A char group info is an on-disk representation of a node.
+ * 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 char group info to be written.
+ * @param info the PtNode info to be written.
* @return the size written, in bytes.
*/
- public static int writeCharGroup(final OutputStream destination, final CharGroupInfo info)
+ private static int writePtNode(final OutputStream destination, final PtNodeInfo info)
throws IOException {
- int size = FormatSpec.GROUP_FLAGS_SIZE;
+ 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;
@@ -503,7 +390,7 @@ public final class BinaryDictIOUtils {
}
}
if (info.mCharacters.length > 1) {
- destination.write((byte)FormatSpec.GROUP_CHARACTERS_TERMINATOR);
+ destination.write((byte)FormatSpec.PTNODE_CHARACTERS_TERMINATOR);
size++;
}
@@ -513,7 +400,7 @@ public final class BinaryDictIOUtils {
}
if (DBG) {
- MakedictLog.d("writeCharGroup origin=" + info.mOriginalAddress + ", size=" + size
+ MakedictLog.d("writePtNode origin=" + info.mOriginalAddress + ", size=" + size
+ ", child=" + info.mChildrenAddress + ", characters ="
+ new String(info.mCharacters, 0, info.mCharacters.length));
}
@@ -524,14 +411,14 @@ public final class BinaryDictIOUtils {
if (info.mShortcutTargets != null && info.mShortcutTargets.size() > 0) {
final int shortcutListSize =
- BinaryDictInputOutput.getShortcutListSize(info.mShortcutTargets);
+ 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)BinaryDictInputOutput.makeShortcutFlags(
+ destination.write((byte)BinaryDictEncoderUtils.makeShortcutFlags(
shortcutIterator.hasNext(), target.mFrequency));
size++;
size += writeString(destination, target.mWord);
@@ -540,28 +427,28 @@ public final class BinaryDictIOUtils {
if (info.mBigrams != null) {
// TODO: Consolidate this code with the code that computes the size of the bigram list
- // in BinaryDictionaryInputOutput#computeActualNodeSize
+ // 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_ATTRIBUTE_HAS_NEXT : 0;
+ ? 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_ATTRIBUTE_OFFSET_NEGATIVE : 0;
- switch (BinaryDictInputOutput.getByteSize(bigramOffset)) {
+ bigramFlags |= (bigramOffset < 0) ? FormatSpec.FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE : 0;
+ switch (BinaryDictEncoderUtils.getByteSize(bigramOffset)) {
case 1:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
+ bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE;
break;
case 2:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
+ bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES;
break;
case 3:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
+ bigramFlags |= FormatSpec.FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES;
break;
}
- bigramFlags |= bigramFrequency & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY;
+ bigramFlags |= bigramFrequency & FormatSpec.FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY;
destination.write((byte)bigramFlags);
size += writeVariableAddress(destination, Math.abs(bigramOffset));
}
@@ -569,82 +456,40 @@ public final class BinaryDictIOUtils {
return size;
}
- @SuppressWarnings("unused")
- private static void updateForwardLink(final FusionDictionaryBufferInterface buffer,
- final int nodeOriginAddress, final int newNodeAddress,
- final FormatOptions formatOptions) {
- buffer.position(nodeOriginAddress);
- int jumpCount = 0;
- while (jumpCount++ < MAX_JUMPS) {
- final int count = BinaryDictInputOutput.readCharGroupCount(buffer);
- for (int i = 0; i < count; ++i) skipCharGroup(buffer, formatOptions);
- final int forwardLinkAddress = buffer.readUnsignedInt24();
- if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
- buffer.position(buffer.position() - FormatSpec.FORWARD_LINK_ADDRESS_SIZE);
- writeSInt24ToBuffer(buffer, newNodeAddress);
- return;
- }
- buffer.position(forwardLinkAddress);
- }
- if (DBG && jumpCount >= MAX_JUMPS) {
- throw new RuntimeException("too many jumps, probably a bug.");
- }
- }
-
/**
- * Helper method to move a char group to the tail of the file.
+ * Compute the size of the PtNode.
*/
- private static int moveCharGroup(final OutputStream destination,
- final FusionDictionaryBufferInterface buffer, final CharGroupInfo info,
- final int nodeOriginAddress, final int oldGroupAddress,
- final FormatOptions formatOptions) throws IOException {
- updateParentAddress(buffer, oldGroupAddress, buffer.limit() + 1, formatOptions);
- buffer.position(oldGroupAddress);
- final int currentFlags = buffer.readUnsignedByte();
- buffer.position(oldGroupAddress);
- buffer.put((byte)(FormatSpec.FLAG_IS_MOVED | (currentFlags
- & (~FormatSpec.MASK_MOVE_AND_DELETE_FLAG))));
- int size = FormatSpec.GROUP_FLAGS_SIZE;
- updateForwardLink(buffer, nodeOriginAddress, buffer.limit(), formatOptions);
- size += writeNode(destination, new CharGroupInfo[] { info });
- return size;
- }
-
- /**
- * Compute the size of the char group.
- */
- private static int computeGroupSize(final CharGroupInfo info,
- final FormatOptions formatOptions) {
- int size = FormatSpec.GROUP_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE
- + BinaryDictInputOutput.getGroupCharactersSize(info.mCharacters)
- + BinaryDictInputOutput.getChildrenAddressSize(info.mFlags, formatOptions);
+ 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.GROUP_FREQUENCY_SIZE;
+ size += FormatSpec.PTNODE_FREQUENCY_SIZE;
}
if (info.mShortcutTargets != null && !info.mShortcutTargets.isEmpty()) {
- size += BinaryDictInputOutput.getShortcutListSize(info.mShortcutTargets);
+ size += BinaryDictEncoderUtils.getShortcutListSize(info.mShortcutTargets);
}
if (info.mBigrams != null) {
for (final PendingAttribute attr : info.mBigrams) {
- size += FormatSpec.GROUP_FLAGS_SIZE;
- size += BinaryDictInputOutput.getByteSize(attr.mAddress);
+ size += FormatSpec.PTNODE_FLAGS_SIZE;
+ size += BinaryDictEncoderUtils.getByteSize(attr.mAddress);
}
}
return size;
}
/**
- * Write a node to the stream.
+ * Write a node array to the stream.
*
* @param destination the stream to write.
- * @param infos groups to be written.
+ * @param infos an array of PtNodeInfo to be written.
* @return the size written, in bytes.
* @throws IOException
*/
- private static int writeNode(final OutputStream destination, final CharGroupInfo[] infos)
+ static int writeNodes(final OutputStream destination, final PtNodeInfo[] infos)
throws IOException {
- int size = BinaryDictInputOutput.getGroupCountSize(infos.length);
- switch (BinaryDictInputOutput.getGroupCountSize(infos.length)) {
+ int size = getPtNodeCountSize(infos.length);
+ switch (getPtNodeCountSize(infos.length)) {
case 1:
destination.write((byte)infos.length);
break;
@@ -653,335 +498,14 @@ public final class BinaryDictIOUtils {
destination.write((byte)(infos.length & 0xFF));
break;
default:
- throw new RuntimeException("Invalid group count size.");
+ throw new RuntimeException("Invalid node count size.");
}
- for (final CharGroupInfo info : infos) size += writeCharGroup(destination, info);
+ for (final PtNodeInfo info : infos) size += writePtNode(destination, info);
writeSInt24ToStream(destination, FormatSpec.NO_FORWARD_LINK_ADDRESS);
return size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
}
- /**
- * Move a group that is referred to by oldGroupOrigin to the tail of the file.
- * And set the children address to the byte after the group.
- *
- * @param nodeOrigin the address of the tail of the file.
- * @param characters
- * @param length
- * @param flags
- * @param frequency
- * @param parentAddress
- * @param shortcutTargets
- * @param bigrams
- * @param destination the stream representing the tail of the file.
- * @param buffer the buffer representing the (constant-size) body of the file.
- * @param oldNodeOrigin
- * @param oldGroupOrigin
- * @param formatOptions
- * @return the size written, in bytes.
- * @throws IOException
- */
- private static int moveGroup(final int nodeOrigin, final int[] characters, final int length,
- final int flags, final int frequency, final int parentAddress,
- final ArrayList<WeightedString> shortcutTargets,
- final ArrayList<PendingAttribute> bigrams, final OutputStream destination,
- final FusionDictionaryBufferInterface buffer, final int oldNodeOrigin,
- final int oldGroupOrigin, final FormatOptions formatOptions) throws IOException {
- int size = 0;
- final int newGroupOrigin = nodeOrigin + 1;
- final int[] writtenCharacters = Arrays.copyOfRange(characters, 0, length);
- final CharGroupInfo tmpInfo = new CharGroupInfo(newGroupOrigin, -1 /* endAddress */,
- flags, writtenCharacters, frequency, parentAddress, FormatSpec.NO_CHILDREN_ADDRESS,
- shortcutTargets, bigrams);
- size = computeGroupSize(tmpInfo, formatOptions);
- final CharGroupInfo newInfo = new CharGroupInfo(newGroupOrigin, newGroupOrigin + size,
- flags, writtenCharacters, frequency, parentAddress,
- nodeOrigin + 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE, shortcutTargets,
- bigrams);
- moveCharGroup(destination, buffer, newInfo, oldNodeOrigin, oldGroupOrigin, formatOptions);
- return 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
- }
-
- /**
- * Insert a word into a binary dictionary.
- *
- * @param buffer
- * @param destination
- * @param word
- * @param frequency
- * @param bigramStrings
- * @param shortcuts
- * @throws IOException
- * @throws UnsupportedFormatException
- */
- // TODO: Support batch insertion.
- // TODO: Remove @UsedForTesting once UserHistoryDictionary is implemented by BinaryDictionary.
- @UsedForTesting
- public static void insertWord(final FusionDictionaryBufferInterface buffer,
- 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 = new ArrayList<PendingAttribute>();
- if (bigramStrings != null) {
- for (final WeightedString bigram : bigramStrings) {
- int position = getTerminalPosition(buffer, bigram.mWord);
- if (position == FormatSpec.NOT_VALID_WORD) {
- // TODO: figure out what is the correct thing to do here.
- } else {
- bigrams.add(new PendingAttribute(bigram.mFrequency, position));
- }
- }
- }
-
- final boolean isTerminal = true;
- final boolean hasBigrams = !bigrams.isEmpty();
- final boolean hasShortcuts = shortcuts != null && !shortcuts.isEmpty();
-
- // find the insert position of the word.
- if (buffer.position() != 0) buffer.position(0);
- final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
-
- int wordPos = 0, address = buffer.position(), nodeOriginAddress = buffer.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 = buffer.position();
- int nodeParentAddress = -1;
- final int charGroupCount = BinaryDictInputOutput.readCharGroupCount(buffer);
- boolean foundNextGroup = false;
-
- for (int i = 0; i < charGroupCount; ++i) {
- address = buffer.position();
- final CharGroupInfo currentInfo = BinaryDictInputOutput.readCharGroup(buffer,
- buffer.position(), header.mFormatOptions);
- final boolean isMovedGroup = BinaryDictInputOutput.isMovedGroup(currentInfo.mFlags,
- header.mFormatOptions);
- if (isMovedGroup) 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 = buffer.limit();
- final int flags = BinaryDictInputOutput.makeCharGroupFlags(p > 1,
- isTerminal, 0, hasShortcuts, hasBigrams, false /* isNotAWord */,
- false /* isBlackListEntry */, header.mFormatOptions);
- int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p, flags,
- frequency, nodeParentAddress, shortcuts, bigrams, destination,
- buffer, nodeOriginAddress, address, header.mFormatOptions);
-
- final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p,
- currentInfo.mCharacters.length);
- if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
- updateParentAddresses(buffer, currentInfo.mChildrenAddress,
- newNodeAddress + written + 1, header.mFormatOptions);
- }
- final CharGroupInfo newInfo2 = new CharGroupInfo(
- newNodeAddress + written + 1, -1 /* endAddress */,
- currentInfo.mFlags, characters2, currentInfo.mFrequency,
- newNodeAddress + 1, currentInfo.mChildrenAddress,
- currentInfo.mShortcutTargets, currentInfo.mBigrams);
- writeNode(destination, new CharGroupInfo[] { 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 = buffer.limit();
- final int childrenAddress = currentInfo.mChildrenAddress;
-
- // move prefix
- final int prefixFlags = BinaryDictInputOutput.makeCharGroupFlags(p > 1,
- false /* isTerminal */, 0 /* childrenAddressSize*/,
- false /* hasShortcut */, false /* hasBigrams */,
- false /* isNotAWord */, false /* isBlackListEntry */,
- header.mFormatOptions);
- int written = moveGroup(newNodeAddress, currentInfo.mCharacters, p,
- prefixFlags, -1 /* frequency */, nodeParentAddress, null, null,
- destination, buffer, nodeOriginAddress, address,
- header.mFormatOptions);
-
- final int[] suffixCharacters = Arrays.copyOfRange(
- currentInfo.mCharacters, p, currentInfo.mCharacters.length);
- if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
- updateParentAddresses(buffer, currentInfo.mChildrenAddress,
- newNodeAddress + written + 1, header.mFormatOptions);
- }
- final int suffixFlags = BinaryDictInputOutput.makeCharGroupFlags(
- 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, header.mFormatOptions);
- final CharGroupInfo suffixInfo = new CharGroupInfo(
- newNodeAddress + written + 1, -1 /* endAddress */, suffixFlags,
- suffixCharacters, currentInfo.mFrequency, newNodeAddress + 1,
- currentInfo.mChildrenAddress, currentInfo.mShortcutTargets,
- currentInfo.mBigrams);
- written += computeGroupSize(suffixInfo, header.mFormatOptions) + 1;
-
- final int[] newCharacters = Arrays.copyOfRange(codePoints, wordPos + p,
- codePoints.length);
- final int flags = BinaryDictInputOutput.makeCharGroupFlags(
- newCharacters.length > 1, isTerminal,
- 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, header.mFormatOptions);
- final CharGroupInfo newInfo = new CharGroupInfo(
- newNodeAddress + written, -1 /* endAddress */, flags,
- newCharacters, frequency, newNodeAddress + 1,
- FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
- writeNode(destination, new CharGroupInfo[] { suffixInfo, newInfo });
- return;
- }
- matched = false;
- break;
- }
- }
-
- if (matched) {
- if (wordPos + currentInfo.mCharacters.length == wordLen) {
- // the word exists in the dictionary.
- // only update group.
- final int newNodeAddress = buffer.limit();
- final boolean hasMultipleChars = currentInfo.mCharacters.length > 1;
- final int flags = BinaryDictInputOutput.makeCharGroupFlags(hasMultipleChars,
- isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, header.mFormatOptions);
- final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1,
- -1 /* endAddress */, flags, currentInfo.mCharacters, frequency,
- nodeParentAddress, currentInfo.mChildrenAddress, shortcuts,
- bigrams);
- moveCharGroup(destination, buffer, newInfo, nodeOriginAddress, address,
- header.mFormatOptions);
- return;
- }
- wordPos += currentInfo.mCharacters.length;
- if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) {
- /*
- * found the prefix of the word.
- * make new node and link to the node from this group.
- *
- * before
- * ab - cd
- *
- * insert "abcde"
- *
- * after
- * ab - cd - e
- */
- final int newNodeAddress = buffer.limit();
- updateChildrenAddress(buffer, address, newNodeAddress,
- header.mFormatOptions);
- final int newGroupAddress = newNodeAddress + 1;
- final boolean hasMultipleChars = (wordLen - wordPos) > 1;
- final int flags = BinaryDictInputOutput.makeCharGroupFlags(hasMultipleChars,
- isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, header.mFormatOptions);
- final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
- final CharGroupInfo newInfo = new CharGroupInfo(newGroupAddress, -1, flags,
- characters, frequency, address, FormatSpec.NO_CHILDREN_ADDRESS,
- shortcuts, bigrams);
- writeNode(destination, new CharGroupInfo[] { newInfo });
- return;
- }
- buffer.position(currentInfo.mChildrenAddress);
- foundNextGroup = true;
- break;
- }
- }
-
- if (foundNextGroup) continue;
-
- // reached the end of the array.
- final int linkAddressPosition = buffer.position();
- int nextLink = buffer.readUnsignedInt24();
- if ((nextLink & MSB24) != 0) {
- nextLink = -(nextLink & 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 = buffer.limit();
- buffer.position(linkAddressPosition);
- writeSInt24ToBuffer(buffer, newNodeAddress);
-
- final int[] characters = Arrays.copyOfRange(codePoints, wordPos, wordLen);
- final int flags = BinaryDictInputOutput.makeCharGroupFlags(characters.length > 1,
- isTerminal, 0 /* childrenAddressSize */, hasShortcuts, hasBigrams,
- isNotAWord, isBlackListEntry, header.mFormatOptions);
- final CharGroupInfo newInfo = new CharGroupInfo(newNodeAddress + 1,
- -1 /* endAddress */, flags, characters, frequency, nodeParentAddress,
- FormatSpec.NO_CHILDREN_ADDRESS, shortcuts, bigrams);
- writeNode(destination, new CharGroupInfo[]{ newInfo });
- return;
- } else {
- depth--;
- buffer.position(nextLink);
- }
- }
- }
-
- /**
- * Find a word from the buffer.
- *
- * @param buffer the buffer representing the body of the dictionary file.
- * @param word the word searched
- * @return the found group
- * @throws IOException
- * @throws UnsupportedFormatException
- */
- @UsedForTesting
- public static CharGroupInfo findWordFromBuffer(final FusionDictionaryBufferInterface buffer,
- final String word) throws IOException, UnsupportedFormatException {
- int position = getTerminalPosition(buffer, word);
- if (position != FormatSpec.NOT_VALID_WORD) {
- buffer.position(0);
- final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
- buffer.position(position);
- return BinaryDictInputOutput.readCharGroup(buffer, position, header.mFormatOptions);
- }
- return null;
- }
-
+ private static final int HEADER_READING_BUFFER_SIZE = 16384;
/**
* Convenience method to read the header of a binary file.
*
@@ -991,21 +515,27 @@ public final class BinaryDictIOUtils {
* @param offset The offset in the file where to start reading the data.
* @param length The length of the data file.
*/
- private static final int HEADER_READING_BUFFER_SIZE = 16384;
- public static FileHeader getDictionaryFileHeader(
+ 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 FileInputStream inStream = new FileInputStream(file);
- try {
- inStream.read(buffer);
- final BinaryDictInputOutput.ByteBufferWrapper wrapper =
- new BinaryDictInputOutput.ByteBufferWrapper(inStream.getChannel().map(
- FileChannel.MapMode.READ_ONLY, offset, length));
- return BinaryDictInputOutput.readHeader(wrapper);
- } finally {
- inStream.close();
- }
+ 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,
@@ -1019,4 +549,83 @@ public final class BinaryDictIOUtils {
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/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
deleted file mode 100644
index 467f6a053..000000000
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
+++ /dev/null
@@ -1,1755 +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.makedict.FormatSpec.FileHeader;
-import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
-import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
-import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
-import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
-import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
-
-import java.io.ByteArrayOutputStream;
-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.Arrays;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.TreeMap;
-
-/**
- * Reads and writes XML files for a FusionDictionary.
- *
- * All the methods in this class are static.
- */
-public final class BinaryDictInputOutput {
-
- private static final boolean DBG = MakedictLog.DBG;
-
- // 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;
- private static final int MAX_JUMPS = 12;
-
- @UsedForTesting
- public interface FusionDictionaryBufferInterface {
- 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();
- public int capacity();
- }
-
- public static final class ByteBufferWrapper implements FusionDictionaryBufferInterface {
- private ByteBuffer mBuffer;
-
- public ByteBufferWrapper(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.
- */
- private 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.
- */
- private 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.
- */
- private 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.GROUP_CHARACTERS_TERMINATOR;
- return index - origin;
- }
-
- /**
- * Writes a string with our character format to a ByteArrayOutputStream.
- *
- * This will also write the terminator byte.
- *
- * @param buffer the ByteArrayOutputStream to write to.
- * @param word the string to write.
- */
- private static void writeString(final ByteArrayOutputStream buffer, final String word) {
- 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.GROUP_CHARACTERS_TERMINATOR);
- }
-
- /**
- * Reads a string from a buffer. This is the converse of the above method.
- */
- private static String readString(final FusionDictionaryBufferInterface buffer) {
- final StringBuilder s = new StringBuilder();
- int character = readChar(buffer);
- while (character != FormatSpec.INVALID_CHARACTER) {
- s.appendCodePoint(character);
- character = readChar(buffer);
- }
- return s.toString();
- }
-
- /**
- * Reads a character from the buffer.
- *
- * This follows the character format documented earlier in this source file.
- *
- * @param buffer the buffer, positioned over an encoded character.
- * @return the character code.
- */
- static int readChar(final FusionDictionaryBufferInterface buffer) {
- int character = buffer.readUnsignedByte();
- if (!fitsOnOneByte(character)) {
- if (FormatSpec.GROUP_CHARACTERS_TERMINATOR == character) {
- return FormatSpec.INVALID_CHARACTER;
- }
- character <<= 16;
- character += buffer.readUnsignedShort();
- }
- return character;
- }
- }
-
- /**
- * 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 getGroupCharactersSize(final int[] characters) {
- int size = CharEncoding.getCharArraySize(characters);
- if (characters.length > 1) size += FormatSpec.GROUP_TERMINATOR_SIZE;
- return size;
- }
-
- /**
- * Compute the binary size of the character array in a group
- *
- * 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 group the group
- * @return the size of the char array, including the terminator if any
- */
- private static int getGroupCharactersSize(final CharGroup group) {
- return getGroupCharactersSize(group.mChars);
- }
-
- /**
- * Compute the binary size of the group count
- * @param count the group count
- * @return the size of the group count, either 1 or 2 bytes.
- */
- public static int getGroupCountSize(final int count) {
- if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) {
- return 1;
- } else if (FormatSpec.MAX_CHARGROUPS_IN_A_NODE >= count) {
- return 2;
- } else {
- throw new RuntimeException("Can't have more than "
- + FormatSpec.MAX_CHARGROUPS_IN_A_NODE + " groups in a node (found " + count
- + ")");
- }
- }
-
- /**
- * Compute the binary size of the group count for a node
- * @param node the node
- * @return the size of the group count, either 1 or 2 bytes.
- */
- private static int getGroupCountSize(final Node node) {
- return getGroupCountSize(node.mData.size());
- }
-
- /**
- * Compute the size of a shortcut in bytes.
- */
- private static int getShortcutSize(final WeightedString shortcut) {
- int size = FormatSpec.GROUP_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.GROUP_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) return 0;
- int size = FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE;
- for (final WeightedString shortcut : shortcutList) {
- size += getShortcutSize(shortcut);
- }
- return size;
- }
-
- /**
- * Compute the maximum size of a CharGroup, assuming 3-byte addresses for everything.
- *
- * @param group the CharGroup to compute the size of.
- * @param options file format options.
- * @return the maximum size of the group.
- */
- private static int getCharGroupMaximumSize(final CharGroup group, final FormatOptions options) {
- int size = getGroupHeaderSize(group, options);
- // If terminal, one byte for the frequency
- if (group.isTerminal()) size += FormatSpec.GROUP_FREQUENCY_SIZE;
- size += FormatSpec.GROUP_MAX_ADDRESS_SIZE; // For children address
- size += getShortcutListSize(group.mShortcutTargets);
- if (null != group.mBigrams) {
- size += (FormatSpec.GROUP_ATTRIBUTE_FLAGS_SIZE
- + FormatSpec.GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE)
- * group.mBigrams.size();
- }
- return size;
- }
-
- /**
- * Compute the maximum size of a node, assuming 3-byte addresses for everything, and caches
- * it in the 'actualSize' member of the node.
- *
- * @param node the node to compute the maximum size of.
- * @param options file format options.
- */
- private static void setNodeMaximumSize(final Node node, final FormatOptions options) {
- int size = getGroupCountSize(node);
- for (CharGroup g : node.mData) {
- final int groupSize = getCharGroupMaximumSize(g, options);
- g.mCachedSize = groupSize;
- size += groupSize;
- }
- if (options.mSupportsDynamicUpdate) {
- size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
- }
- node.mCachedSize = size;
- }
-
- /**
- * 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 group is moved.
- */
- public static boolean isMovedGroup(final int flags, final FormatOptions options) {
- return options.mSupportsDynamicUpdate
- && ((flags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) == FormatSpec.FLAG_IS_MOVED);
- }
-
- /**
- * Helper method to check whether the group is deleted.
- */
- public static boolean isDeletedGroup(final int flags, final FormatOptions formatOptions) {
- return formatOptions.mSupportsDynamicUpdate
- && ((flags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) == FormatSpec.FLAG_IS_DELETED);
- }
-
- /**
- * 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;
- }
-
- /**
- * Compute the size of the header (flag + [parent address] + characters size) of a CharGroup.
- *
- * @param group the group of which to compute the size of the header
- * @param options file format options.
- */
- private static int getGroupHeaderSize(final CharGroup group, final FormatOptions options) {
- if (supportsDynamicUpdate(options)) {
- return FormatSpec.GROUP_FLAGS_SIZE + FormatSpec.PARENT_ADDRESS_SIZE
- + getGroupCharactersSize(group);
- } else {
- return FormatSpec.GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
- }
- }
-
- private static final int UINT8_MAX = 0xFF;
- private static final int UINT16_MAX = 0xFFFF;
- private static final int UINT24_MAX = 0xFFFFFF;
-
- /**
- * 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 <= UINT24_MAX);
- if (!hasChildrenAddress(address)) {
- return 0;
- } else if (Math.abs(address) <= UINT8_MAX) {
- return 1;
- } else if (Math.abs(address) <= UINT16_MAX) {
- return 2;
- } else {
- return 3;
- }
- }
-
- private static final int SINT24_MAX = 0x7FFFFF;
- private static final int MSB8 = 0x80;
- private static final int MSB24 = 0x800000;
-
- // 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<Node> flattenTree(final Node root) {
- final int treeSize = FusionDictionary.countCharGroups(root);
- MakedictLog.i("Counted nodes : " + treeSize);
- final ArrayList<Node> flatTree = new ArrayList<Node>(treeSize);
- return flattenTreeInner(flatTree, root);
- }
-
- private static ArrayList<Node> flattenTreeInner(final ArrayList<Node> list, final Node node) {
- // 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(node);
- final ArrayList<CharGroup> branches = node.mData;
- final int nodeSize = branches.size();
- for (CharGroup group : branches) {
- if (null != group.mChildren) flattenTreeInner(list, group.mChildren);
- }
- return list;
- }
-
- /**
- * Finds the absolute address of a word in the dictionary.
- *
- * @param dict the dictionary in which to search.
- * @param word the word we are searching for.
- * @return the word address. If it is not found, an exception is thrown.
- */
- private static int findAddressOfWord(final FusionDictionary dict, final String word) {
- return FusionDictionary.findWordInTree(dict.mRoot, word).mCachedAddress;
- }
-
- /**
- * Computes the actual node size, based on the cached addresses of the children nodes.
- *
- * Each node stores its tentative address. During dictionary address computing, these
- * are not final, but they can be used to compute the node size (the node 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
- * contents (as in, any of the addresses stored in the cache fields) have changed with
- * respect to their previous value.
- *
- * @param node the node 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 changed, true otherwise.
- */
- private static boolean computeActualNodeSize(final Node node, final FusionDictionary dict,
- final FormatOptions formatOptions) {
- boolean changed = false;
- int size = getGroupCountSize(node);
- for (CharGroup group : node.mData) {
- if (group.mCachedAddress != node.mCachedAddress + size) {
- changed = true;
- group.mCachedAddress = node.mCachedAddress + size;
- }
- int groupSize = getGroupHeaderSize(group, formatOptions);
- if (group.isTerminal()) groupSize += FormatSpec.GROUP_FREQUENCY_SIZE;
- if (null == group.mChildren && formatOptions.mSupportsDynamicUpdate) {
- groupSize += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
- } else if (null != group.mChildren) {
- final int offsetBasePoint = groupSize + node.mCachedAddress + size;
- final int offset = group.mChildren.mCachedAddress - offsetBasePoint;
- // assign my address to children's parent address
- group.mChildren.mCachedParentAddress = group.mCachedAddress
- - group.mChildren.mCachedAddress;
- if (formatOptions.mSupportsDynamicUpdate) {
- groupSize += FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
- } else {
- groupSize += getByteSize(offset);
- }
- }
- groupSize += getShortcutListSize(group.mShortcutTargets);
- if (null != group.mBigrams) {
- for (WeightedString bigram : group.mBigrams) {
- final int offsetBasePoint = groupSize + node.mCachedAddress + size
- + FormatSpec.GROUP_FLAGS_SIZE;
- final int addressOfBigram = findAddressOfWord(dict, bigram.mWord);
- final int offset = addressOfBigram - offsetBasePoint;
- groupSize += getByteSize(offset) + FormatSpec.GROUP_FLAGS_SIZE;
- }
- }
- group.mCachedSize = groupSize;
- size += groupSize;
- }
- if (formatOptions.mSupportsDynamicUpdate) {
- size += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
- }
- if (node.mCachedSize != size) {
- node.mCachedSize = size;
- changed = true;
- }
- return changed;
- }
-
- /**
- * Computes the byte size of a list of nodes and updates each node cached position.
- *
- * @param flatNodes the array of nodes.
- * @param formatOptions file format options.
- * @return the byte size of the entire stack.
- */
- private static int stackNodes(final ArrayList<Node> flatNodes,
- final FormatOptions formatOptions) {
- int nodeOffset = 0;
- for (Node n : flatNodes) {
- n.mCachedAddress = nodeOffset;
- int groupCountSize = getGroupCountSize(n);
- int groupOffset = 0;
- for (CharGroup g : n.mData) {
- g.mCachedAddress = groupCountSize + nodeOffset + groupOffset;
- groupOffset += g.mCachedSize;
- }
- final int nodeSize = groupCountSize + groupOffset
- + (formatOptions.mSupportsDynamicUpdate
- ? FormatSpec.FORWARD_LINK_ADDRESS_SIZE : 0);
- if (nodeSize != n.mCachedSize) {
- throw new RuntimeException("Bug : Stored and computed node size differ");
- }
- nodeOffset += n.mCachedSize;
- }
- return nodeOffset;
- }
-
- /**
- * Compute the addresses and sizes of an ordered node array.
- *
- * This method takes a node array and will update its cached address and size values
- * so that they can be written into a file. It determines the smallest size each of the
- * nodes can be given the addresses of its children and attributes, and store that into
- * each node.
- * The order of the node 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 array of nodes
- * @param formatOptions file format options.
- * @return the same array it was passed. The nodes have been updated for address and size.
- */
- private static ArrayList<Node> computeAddresses(final FusionDictionary dict,
- final ArrayList<Node> flatNodes, final FormatOptions formatOptions) {
- // First get the worst sizes and offsets
- for (Node n : flatNodes) setNodeMaximumSize(n, formatOptions);
- final int offset = stackNodes(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;
- for (Node n : flatNodes) {
- final int oldNodeSize = n.mCachedSize;
- final boolean changed = computeActualNodeSize(n, dict, formatOptions);
- final int newNodeSize = n.mCachedSize;
- if (oldNodeSize < newNodeSize) throw new RuntimeException("Increased size ?!");
- changesDone |= changed;
- }
- stackNodes(flatNodes, formatOptions);
- ++passes;
- if (passes > MAX_PASSES) throw new RuntimeException("Too many passes - probably a bug");
- } while (changesDone);
-
- final Node lastNode = flatNodes.get(flatNodes.size() - 1);
- MakedictLog.i("Compression complete in " + passes + " passes.");
- MakedictLog.i("After address compression : "
- + (lastNode.mCachedAddress + lastNode.mCachedSize));
-
- return flatNodes;
- }
-
- /**
- * Sanity-checking method.
- *
- * This method checks an array of node for juxtaposition, that is, it will do
- * nothing if each node's cached address is actually the previous node's address
- * plus the previous node's size.
- * If this is not the case, it will throw an exception.
- *
- * @param array the array node to check
- */
- private static void checkFlatNodeArray(final ArrayList<Node> array) {
- int offset = 0;
- int index = 0;
- for (Node n : array) {
- if (n.mCachedAddress != offset) {
- throw new RuntimeException("Wrong address for node " + index
- + " : expected " + offset + ", got " + n.mCachedAddress);
- }
- ++index;
- offset += n.mCachedSize;
- }
- }
-
- /**
- * Helper method to write a variable-size address to a file.
- *
- * @param buffer the buffer to write to.
- * @param index the index in the buffer to write the address to.
- * @param address the address to write.
- * @return the size in bytes the address actually took.
- */
- private static int writeVariableAddress(final byte[] buffer, int index, final int address) {
- switch (getByteSize(address)) {
- case 1:
- buffer[index++] = (byte)address;
- return 1;
- case 2:
- buffer[index++] = (byte)(0xFF & (address >> 8));
- buffer[index++] = (byte)(0xFF & address);
- return 2;
- case 3:
- buffer[index++] = (byte)(0xFF & (address >> 16));
- buffer[index++] = (byte)(0xFF & (address >> 8));
- buffer[index++] = (byte)(0xFF & address);
- return 3;
- case 0:
- return 0;
- default:
- throw new RuntimeException("Address " + address + " has a strange size");
- }
- }
-
- /**
- * Helper method to write a variable-size signed address to a file.
- *
- * @param buffer the buffer to write to.
- * @param index the index in the buffer to write the address to.
- * @param address the address to write.
- * @return the size in bytes the address actually took.
- */
- private static int writeVariableSignedAddress(final byte[] buffer, int index,
- final int address) {
- if (!hasChildrenAddress(address)) {
- buffer[index] = buffer[index + 1] = buffer[index + 2] = 0;
- } else {
- final int absAddress = Math.abs(address);
- buffer[index++] = (byte)((address < 0 ? MSB8 : 0) | (0xFF & (absAddress >> 16)));
- buffer[index++] = (byte)(0xFF & (absAddress >> 8));
- buffer[index++] = (byte)(0xFF & absAddress);
- }
- return 3;
- }
-
- /**
- * Makes the flag value for a char group.
- *
- * @param hasMultipleChars whether the group has multiple chars.
- * @param isTerminal whether the group is terminal.
- * @param childrenAddressSize the size of a children address.
- * @param hasShortcuts whether the group has shortcuts.
- * @param hasBigrams whether the group has bigrams.
- * @param isNotAWord whether the group is not a word.
- * @param isBlackListEntry whether the group is a blacklist entry.
- * @param formatOptions file format options.
- * @return the flags
- */
- static int makeCharGroupFlags(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_GROUP_ADDRESS_TYPE_ONEBYTE;
- break;
- case 2:
- flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_TWOBYTES;
- break;
- case 3:
- flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_THREEBYTES;
- break;
- case 0:
- flags |= FormatSpec.FLAG_GROUP_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;
- }
-
- private static byte makeCharGroupFlags(final CharGroup group, final int groupAddress,
- final int childrenOffset, final FormatOptions formatOptions) {
- return (byte) makeCharGroupFlags(group.mChars.length > 1, group.mFrequency >= 0,
- getByteSize(childrenOffset), group.mShortcutTargets != null, group.mBigrams != null,
- group.mIsNotAWord, group.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
- */
- private static final int makeBigramFlags(final boolean more, final int offset,
- int bigramFrequency, final int unigramFrequency, final String word) {
- int bigramFlags = (more ? FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT : 0)
- + (offset < 0 ? FormatSpec.FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0);
- switch (getByteSize(offset)) {
- case 1:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
- break;
- case 2:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
- break;
- case 3:
- bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_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_ATTRIBUTE_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_ATTRIBUTE_HAS_NEXT : 0)
- + (frequency & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY);
- }
-
- private static final int writeParentAddress(final byte[] buffer, final int index,
- final int address, final FormatOptions formatOptions) {
- if (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 <= SINT24_MAX);
- buffer[index] = (byte)((address < 0 ? MSB8 : 0)
- | ((absAddress >> 16) & 0xFF));
- buffer[index + 1] = (byte)((absAddress >> 8) & 0xFF);
- buffer[index + 2] = (byte)(absAddress & 0xFF);
- }
- return index + 3;
- } else {
- return index;
- }
- }
-
- /**
- * Write a node to memory. The node is expected to have its final position cached.
- *
- * This can be an empty map, but the more is inside the faster the lookups will be. It can
- * be carried on as long as nodes do not move.
- *
- * @param dict the dictionary the node is a part of (for relative offsets).
- * @param buffer the memory buffer to write to.
- * @param node the node to write.
- * @param formatOptions file format options.
- * @return the address of the END of the node.
- */
- @SuppressWarnings("unused")
- private static int writePlacedNode(final FusionDictionary dict, byte[] buffer,
- final Node node, final FormatOptions formatOptions) {
- // TODO: Make the code in common with BinaryDictIOUtils#writeCharGroup
- int index = node.mCachedAddress;
-
- final int groupCount = node.mData.size();
- final int countSize = getGroupCountSize(node);
- final int parentAddress = node.mCachedParentAddress;
- if (1 == countSize) {
- buffer[index++] = (byte)groupCount;
- } else if (2 == countSize) {
- // We need to signal 2-byte size by setting the top bit of the MSB to 1, so
- // we | 0x80 to do this.
- buffer[index++] = (byte)((groupCount >> 8) | 0x80);
- buffer[index++] = (byte)(groupCount & 0xFF);
- } else {
- throw new RuntimeException("Strange size from getGroupCountSize : " + countSize);
- }
- int groupAddress = index;
- for (int i = 0; i < groupCount; ++i) {
- CharGroup group = node.mData.get(i);
- if (index != group.mCachedAddress) throw new RuntimeException("Bug: write index is not "
- + "the same as the cached address of the group : "
- + index + " <> " + group.mCachedAddress);
- groupAddress += getGroupHeaderSize(group, formatOptions);
- // Sanity checks.
- if (DBG && group.mFrequency > FormatSpec.MAX_TERMINAL_FREQUENCY) {
- throw new RuntimeException("A node has a frequency > "
- + FormatSpec.MAX_TERMINAL_FREQUENCY
- + " : " + group.mFrequency);
- }
- if (group.mFrequency >= 0) groupAddress += FormatSpec.GROUP_FREQUENCY_SIZE;
- final int childrenOffset = null == group.mChildren
- ? FormatSpec.NO_CHILDREN_ADDRESS
- : group.mChildren.mCachedAddress - groupAddress;
- byte flags = makeCharGroupFlags(group, groupAddress, childrenOffset, formatOptions);
- buffer[index++] = flags;
-
- if (parentAddress == FormatSpec.NO_PARENT_ADDRESS) {
- index = writeParentAddress(buffer, index, parentAddress, formatOptions);
- } else {
- index = writeParentAddress(buffer, index,
- parentAddress + (node.mCachedAddress - group.mCachedAddress),
- formatOptions);
- }
-
- index = CharEncoding.writeCharArray(group.mChars, buffer, index);
- if (group.hasSeveralChars()) {
- buffer[index++] = FormatSpec.GROUP_CHARACTERS_TERMINATOR;
- }
- if (group.mFrequency >= 0) {
- buffer[index++] = (byte) group.mFrequency;
- }
-
- final int shift;
- if (formatOptions.mSupportsDynamicUpdate) {
- shift = writeVariableSignedAddress(buffer, index, childrenOffset);
- } else {
- shift = writeVariableAddress(buffer, index, childrenOffset);
- }
- index += shift;
- groupAddress += shift;
-
- // Write shortcuts
- if (null != group.mShortcutTargets) {
- final int indexOfShortcutByteSize = index;
- index += FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE;
- groupAddress += FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE;
- final Iterator<WeightedString> shortcutIterator = group.mShortcutTargets.iterator();
- while (shortcutIterator.hasNext()) {
- final WeightedString target = shortcutIterator.next();
- ++groupAddress;
- int shortcutFlags = makeShortcutFlags(shortcutIterator.hasNext(),
- target.mFrequency);
- buffer[index++] = (byte)shortcutFlags;
- final int shortcutShift = CharEncoding.writeString(buffer, index, target.mWord);
- index += shortcutShift;
- groupAddress += shortcutShift;
- }
- final int shortcutByteSize = index - indexOfShortcutByteSize;
- if (shortcutByteSize > 0xFFFF) {
- throw new RuntimeException("Shortcut list too large");
- }
- buffer[indexOfShortcutByteSize] = (byte)(shortcutByteSize >> 8);
- buffer[indexOfShortcutByteSize + 1] = (byte)(shortcutByteSize & 0xFF);
- }
- // Write bigrams
- if (null != group.mBigrams) {
- final Iterator<WeightedString> bigramIterator = group.mBigrams.iterator();
- while (bigramIterator.hasNext()) {
- final WeightedString bigram = bigramIterator.next();
- final CharGroup target =
- FusionDictionary.findWordInTree(dict.mRoot, bigram.mWord);
- final int addressOfBigram = target.mCachedAddress;
- final int unigramFrequencyForThisWord = target.mFrequency;
- ++groupAddress;
- final int offset = addressOfBigram - groupAddress;
- int bigramFlags = makeBigramFlags(bigramIterator.hasNext(), offset,
- bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord);
- buffer[index++] = (byte)bigramFlags;
- final int bigramShift = writeVariableAddress(buffer, index, Math.abs(offset));
- index += bigramShift;
- groupAddress += bigramShift;
- }
- }
-
- }
- if (formatOptions.mSupportsDynamicUpdate) {
- buffer[index] = buffer[index + 1] = buffer[index + 2]
- = FormatSpec.NO_FORWARD_LINK_ADDRESS;
- index += FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
- }
- if (index != node.mCachedAddress + node.mCachedSize) throw new RuntimeException(
- "Not the same size : written "
- + (index - node.mCachedAddress) + " bytes out of a node that should have "
- + node.mCachedSize + " bytes");
- return index;
- }
-
- /**
- * Dumps a collection of useful statistics about a node array.
- *
- * This prints purely informative stuff, like the total estimated file size, the
- * number of nodes, of character groups, the repartition of each address size, etc
- *
- * @param nodes the node array.
- */
- private static void showStatistics(ArrayList<Node> nodes) {
- int firstTerminalAddress = Integer.MAX_VALUE;
- int lastTerminalAddress = Integer.MIN_VALUE;
- int size = 0;
- int charGroups = 0;
- int maxGroups = 0;
- int maxRuns = 0;
- for (Node n : nodes) {
- if (maxGroups < n.mData.size()) maxGroups = n.mData.size();
- for (CharGroup cg : n.mData) {
- ++charGroups;
- if (cg.mChars.length > maxRuns) maxRuns = cg.mChars.length;
- if (cg.mFrequency >= 0) {
- if (n.mCachedAddress < firstTerminalAddress)
- firstTerminalAddress = n.mCachedAddress;
- if (n.mCachedAddress > lastTerminalAddress)
- lastTerminalAddress = n.mCachedAddress;
- }
- }
- if (n.mCachedAddress + n.mCachedSize > size) size = n.mCachedAddress + n.mCachedSize;
- }
- final int[] groupCounts = new int[maxGroups + 1];
- final int[] runCounts = new int[maxRuns + 1];
- for (Node n : nodes) {
- ++groupCounts[n.mData.size()];
- for (CharGroup cg : n.mData) {
- ++runCounts[cg.mChars.length];
- }
- }
-
- MakedictLog.i("Statistics:\n"
- + " total file size " + size + "\n"
- + " " + nodes.size() + " nodes\n"
- + " " + charGroups + " groups (" + ((float)charGroups / nodes.size())
- + " groups per node)\n"
- + " first terminal at " + firstTerminalAddress + "\n"
- + " last terminal at " + lastTerminalAddress + "\n"
- + " Group stats : max = " + maxGroups);
- for (int i = 0; i < groupCounts.length; ++i) {
- MakedictLog.i(" " + i + " : " + groupCounts[i]);
- }
- MakedictLog.i(" Character run stats : max = " + maxRuns);
- for (int i = 0; i < runCounts.length; ++i) {
- MakedictLog.i(" " + i + " : " + runCounts[i]);
- }
- }
-
- /**
- * Dumps a FusionDictionary to a file.
- *
- * This is the public entry point to write a dictionary to a file.
- *
- * @param destination the stream to write the binary data to.
- * @param dict the dictionary to write.
- * @param formatOptions file format options.
- */
- public static void writeDictionaryBinary(final OutputStream destination,
- final FusionDictionary dict, final FormatOptions formatOptions)
- throws IOException, UnsupportedFormatException {
-
- // Addresses are limited to 3 bytes, but since addresses can be relative to each node, the
- // structure itself is not limited to 16MB. However, if it is over 16MB deciding the order
- // of the nodes becomes a quite complicated problem, because though the dictionary itself
- // does not have a size limit, each node 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.
-
- 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.
- if (version >= FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) {
- // Magic number for version 2+.
- headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 24)));
- headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 16)));
- headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_2_MAGIC_NUMBER >> 8)));
- headerBuffer.write((byte) (0xFF & FormatSpec.VERSION_2_MAGIC_NUMBER));
- // Dictionary version.
- headerBuffer.write((byte) (0xFF & (version >> 8)));
- headerBuffer.write((byte) (0xFF & version));
- } else {
- // Magic number for version 1.
- headerBuffer.write((byte) (0xFF & (FormatSpec.VERSION_1_MAGIC_NUMBER >> 8)));
- headerBuffer.write((byte) (0xFF & FormatSpec.VERSION_1_MAGIC_NUMBER));
- // Dictionary version.
- headerBuffer.write((byte) (0xFF & version));
- }
- // Options flags
- final int options = makeOptionsValue(dict, formatOptions);
- headerBuffer.write((byte) (0xFF & (options >> 8)));
- headerBuffer.write((byte) (0xFF & options));
- if (version >= FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) {
- 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);
- } else {
- headerBuffer.writeTo(destination);
- }
-
- headerBuffer.close();
-
- // Leave the choice of the optimal node order to the flattenTree function.
- MakedictLog.i("Flattening the tree...");
- ArrayList<Node> flatNodes = flattenTree(dict.mRoot);
-
- MakedictLog.i("Computing addresses...");
- computeAddresses(dict, flatNodes, formatOptions);
- MakedictLog.i("Checking array...");
- if (DBG) checkFlatNodeArray(flatNodes);
-
- // Create a buffer that matches the final dictionary size.
- final Node lastNode = flatNodes.get(flatNodes.size() - 1);
- final int bufferSize = lastNode.mCachedAddress + lastNode.mCachedSize;
- final byte[] buffer = new byte[bufferSize];
- int index = 0;
-
- MakedictLog.i("Writing file...");
- int dataEndOffset = 0;
- for (Node n : flatNodes) {
- dataEndOffset = writePlacedNode(dict, buffer, n, formatOptions);
- }
-
- if (DBG) showStatistics(flatNodes);
-
- destination.write(buffer, 0, dataEndOffset);
-
- destination.close();
- MakedictLog.i("Done");
- }
-
-
- // Input methods: Read a binary dictionary to memory.
- // readDictionaryBinary is the public entry point for them.
-
- static int getChildrenAddressSize(final int optionFlags,
- final FormatOptions formatOptions) {
- if (formatOptions.mSupportsDynamicUpdate) return FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE;
- switch (optionFlags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) {
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
- return 1;
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
- return 2;
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
- return 3;
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
- default:
- return 0;
- }
- }
-
- static int readChildrenAddress(final FusionDictionaryBufferInterface buffer,
- final int optionFlags, final FormatOptions options) {
- if (options.mSupportsDynamicUpdate) {
- final int address = buffer.readUnsignedInt24();
- if (address == 0) return FormatSpec.NO_CHILDREN_ADDRESS;
- if ((address & MSB24) != 0) {
- return -(address & SINT24_MAX);
- } else {
- return address;
- }
- }
- int address;
- switch (optionFlags & FormatSpec.MASK_GROUP_ADDRESS_TYPE) {
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
- return buffer.readUnsignedByte();
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
- return buffer.readUnsignedShort();
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
- return buffer.readUnsignedInt24();
- case FormatSpec.FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
- default:
- return FormatSpec.NO_CHILDREN_ADDRESS;
- }
- }
-
- static int readParentAddress(final FusionDictionaryBufferInterface buffer,
- final FormatOptions formatOptions) {
- if (supportsDynamicUpdate(formatOptions)) {
- final int parentAddress = buffer.readUnsignedInt24();
- final int sign = ((parentAddress & MSB24) != 0) ? -1 : 1;
- return sign * (parentAddress & SINT24_MAX);
- } else {
- return FormatSpec.NO_PARENT_ADDRESS;
- }
- }
-
- private static final int[] CHARACTER_BUFFER = new int[FormatSpec.MAX_WORD_LENGTH];
- public static CharGroupInfo readCharGroup(final FusionDictionaryBufferInterface buffer,
- final int originalGroupAddress, final FormatOptions options) {
- int addressPointer = originalGroupAddress;
- final int flags = buffer.readUnsignedByte();
- ++addressPointer;
-
- final int parentAddress = readParentAddress(buffer, options);
- if (supportsDynamicUpdate(options)) {
- addressPointer += 3;
- }
-
- final int characters[];
- if (0 != (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS)) {
- int index = 0;
- int character = CharEncoding.readChar(buffer);
- addressPointer += CharEncoding.getCharSize(character);
- while (-1 != 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 CHARACTER_BUFFER.
- CHARACTER_BUFFER[index++] = character;
- character = CharEncoding.readChar(buffer);
- addressPointer += CharEncoding.getCharSize(character);
- }
- characters = Arrays.copyOfRange(CHARACTER_BUFFER, 0, index);
- } else {
- final int character = CharEncoding.readChar(buffer);
- addressPointer += CharEncoding.getCharSize(character);
- characters = new int[] { character };
- }
- final int frequency;
- if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) {
- ++addressPointer;
- frequency = buffer.readUnsignedByte();
- } else {
- frequency = CharGroup.NOT_A_TERMINAL;
- }
- int childrenAddress = readChildrenAddress(buffer, flags, options);
- if (childrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
- childrenAddress += addressPointer;
- }
- addressPointer += getChildrenAddressSize(flags, options);
- ArrayList<WeightedString> shortcutTargets = null;
- if (0 != (flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)) {
- final int pointerBefore = buffer.position();
- shortcutTargets = new ArrayList<WeightedString>();
- buffer.readUnsignedShort(); // Skip the size
- while (true) {
- final int targetFlags = buffer.readUnsignedByte();
- final String word = CharEncoding.readString(buffer);
- shortcutTargets.add(new WeightedString(word,
- targetFlags & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY));
- if (0 == (targetFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT)) break;
- }
- addressPointer += buffer.position() - pointerBefore;
- }
- ArrayList<PendingAttribute> bigrams = null;
- if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) {
- bigrams = new ArrayList<PendingAttribute>();
- int bigramCount = 0;
- while (bigramCount++ < FormatSpec.MAX_BIGRAMS_IN_A_GROUP) {
- final int bigramFlags = buffer.readUnsignedByte();
- ++addressPointer;
- final int sign = 0 == (bigramFlags & FormatSpec.FLAG_ATTRIBUTE_OFFSET_NEGATIVE)
- ? 1 : -1;
- int bigramAddress = addressPointer;
- switch (bigramFlags & FormatSpec.MASK_ATTRIBUTE_ADDRESS_TYPE) {
- case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
- bigramAddress += sign * buffer.readUnsignedByte();
- addressPointer += 1;
- break;
- case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
- bigramAddress += sign * buffer.readUnsignedShort();
- addressPointer += 2;
- break;
- case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
- final int offset = (buffer.readUnsignedByte() << 16)
- + buffer.readUnsignedShort();
- bigramAddress += sign * offset;
- addressPointer += 3;
- break;
- default:
- throw new RuntimeException("Has bigrams with no address");
- }
- bigrams.add(new PendingAttribute(bigramFlags & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY,
- bigramAddress));
- if (0 == (bigramFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT)) break;
- }
- if (bigramCount >= FormatSpec.MAX_BIGRAMS_IN_A_GROUP) {
- MakedictLog.d("too many bigrams in a group.");
- }
- }
- return new CharGroupInfo(originalGroupAddress, addressPointer, flags, characters, frequency,
- parentAddress, childrenAddress, shortcutTargets, bigrams);
- }
-
- /**
- * Reads and returns the char group count out of a buffer and forwards the pointer.
- */
- public static int readCharGroupCount(final FusionDictionaryBufferInterface buffer) {
- final int msb = buffer.readUnsignedByte();
- if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) {
- return msb;
- } else {
- return ((FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8)
- + buffer.readUnsignedByte();
- }
- }
-
- // The word cache here is a stopgap bandaid to help the catastrophic performance
- // of this method. Since it performs direct, unbuffered random access to the file and
- // may be called hundreds of thousands of times, the resulting performance is not
- // reasonable without some kind of cache. Thus:
- private static TreeMap<Integer, WeightedString> wordCache =
- new TreeMap<Integer, WeightedString>();
- /**
- * Finds, as a string, the word at the address passed as an argument.
- *
- * @param buffer the buffer to read from.
- * @param headerSize the size of the header.
- * @param address the address to seek.
- * @param formatOptions file format options.
- * @return the word with its frequency, as a weighted string.
- */
- /* package for tests */ static WeightedString getWordAtAddress(
- final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
- final FormatOptions formatOptions) {
- final WeightedString cachedString = wordCache.get(address);
- if (null != cachedString) return cachedString;
-
- final WeightedString result;
- final int originalPointer = buffer.position();
- buffer.position(address);
-
- if (supportsDynamicUpdate(formatOptions)) {
- result = getWordAtAddressWithParentAddress(buffer, headerSize, address, formatOptions);
- } else {
- result = getWordAtAddressWithoutParentAddress(buffer, headerSize, address,
- formatOptions);
- }
-
- wordCache.put(address, result);
- buffer.position(originalPointer);
- return result;
- }
-
- // TODO: static!? This will behave erratically when used in multi-threaded code.
- // We need to fix this
- private static int[] sGetWordBuffer = new int[FormatSpec.MAX_WORD_LENGTH];
- @SuppressWarnings("unused")
- private static WeightedString getWordAtAddressWithParentAddress(
- final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
- final FormatOptions options) {
- int currentAddress = address;
- int index = FormatSpec.MAX_WORD_LENGTH - 1;
- int frequency = Integer.MIN_VALUE;
- // 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) {
- CharGroupInfo currentInfo;
- int loopCounter = 0;
- do {
- buffer.position(currentAddress + headerSize);
- currentInfo = readCharGroup(buffer, currentAddress, options);
- if (isMovedGroup(currentInfo.mFlags, options)) {
- currentAddress = currentInfo.mParentAddress + currentInfo.mOriginalAddress;
- }
- if (DBG && loopCounter++ > MAX_JUMPS) {
- MakedictLog.d("Too many jumps - probably a bug");
- }
- } while (isMovedGroup(currentInfo.mFlags, options));
- if (Integer.MIN_VALUE == frequency) frequency = currentInfo.mFrequency;
- for (int i = 0; i < currentInfo.mCharacters.length; ++i) {
- sGetWordBuffer[index--] =
- currentInfo.mCharacters[currentInfo.mCharacters.length - i - 1];
- }
- if (currentInfo.mParentAddress == FormatSpec.NO_PARENT_ADDRESS) break;
- currentAddress = currentInfo.mParentAddress + currentInfo.mOriginalAddress;
- }
-
- return new WeightedString(
- new String(sGetWordBuffer, index + 1, FormatSpec.MAX_WORD_LENGTH - index - 1),
- frequency);
- }
-
- private static WeightedString getWordAtAddressWithoutParentAddress(
- final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
- final FormatOptions options) {
- buffer.position(headerSize);
- final int count = readCharGroupCount(buffer);
- int groupOffset = getGroupCountSize(count);
- final StringBuilder builder = new StringBuilder();
- WeightedString result = null;
-
- CharGroupInfo last = null;
- for (int i = count - 1; i >= 0; --i) {
- CharGroupInfo info = readCharGroup(buffer, groupOffset, options);
- groupOffset = info.mEndAddress;
- if (info.mOriginalAddress == address) {
- builder.append(new String(info.mCharacters, 0, info.mCharacters.length));
- result = new WeightedString(builder.toString(), info.mFrequency);
- break; // and return
- }
- if (hasChildrenAddress(info.mChildrenAddress)) {
- if (info.mChildrenAddress > address) {
- if (null == last) continue;
- builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
- buffer.position(last.mChildrenAddress + headerSize);
- i = readCharGroupCount(buffer);
- groupOffset = last.mChildrenAddress + getGroupCountSize(i);
- last = null;
- continue;
- }
- last = info;
- }
- if (0 == i && hasChildrenAddress(last.mChildrenAddress)) {
- builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
- buffer.position(last.mChildrenAddress + headerSize);
- i = readCharGroupCount(buffer);
- groupOffset = last.mChildrenAddress + getGroupCountSize(i);
- last = null;
- continue;
- }
- }
- return result;
- }
-
- /**
- * Reads a single node from a buffer.
- *
- * This methods reads the file at the current position. A node is fully expected to start at
- * the current position.
- * This will recursively read other nodes into the structure, populating the reverse
- * maps on the fly and using them to keep track of already read nodes.
- *
- * @param buffer the buffer, correctly positioned at the start of a node.
- * @param headerSize the size, in bytes, of the file header.
- * @param reverseNodeMap a mapping from addresses to already read nodes.
- * @param reverseGroupMap a mapping from addresses to already read character groups.
- * @param options file format options.
- * @return the read node with all his children already read.
- */
- private static Node readNode(final FusionDictionaryBufferInterface buffer, final int headerSize,
- final Map<Integer, Node> reverseNodeMap, final Map<Integer, CharGroup> reverseGroupMap,
- final FormatOptions options)
- throws IOException {
- final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>();
- final int nodeOrigin = buffer.position() - headerSize;
-
- do { // Scan the linked-list node.
- final int nodeHeadPosition = buffer.position() - headerSize;
- final int count = readCharGroupCount(buffer);
- int groupOffset = nodeHeadPosition + getGroupCountSize(count);
- for (int i = count; i > 0; --i) { // Scan the array of CharGroup.
- CharGroupInfo info = readCharGroup(buffer, groupOffset, options);
- if (isMovedGroup(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 = getWordAtAddress(
- buffer, headerSize, bigram.mAddress, options);
- final int reconstructedFrequency =
- reconstructBigramFrequency(word.mFrequency, bigram.mFrequency);
- bigrams.add(new WeightedString(word.mWord, reconstructedFrequency));
- }
- }
- if (hasChildrenAddress(info.mChildrenAddress)) {
- Node children = reverseNodeMap.get(info.mChildrenAddress);
- if (null == children) {
- final int currentPosition = buffer.position();
- buffer.position(info.mChildrenAddress + headerSize);
- children = readNode(
- buffer, headerSize, reverseNodeMap, reverseGroupMap, options);
- buffer.position(currentPosition);
- }
- nodeContents.add(
- new CharGroup(info.mCharacters, shortcutTargets, bigrams,
- info.mFrequency,
- 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD),
- 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED), children));
- } else {
- nodeContents.add(
- new CharGroup(info.mCharacters, shortcutTargets, bigrams,
- info.mFrequency,
- 0 != (info.mFlags & FormatSpec.FLAG_IS_NOT_A_WORD),
- 0 != (info.mFlags & FormatSpec.FLAG_IS_BLACKLISTED)));
- }
- groupOffset = info.mEndAddress;
- }
-
- // reach the end of the array.
- if (options.mSupportsDynamicUpdate) {
- final int nextAddress = buffer.readUnsignedInt24();
- if (nextAddress >= 0 && nextAddress < buffer.limit()) {
- buffer.position(nextAddress);
- } else {
- break;
- }
- }
- } while (options.mSupportsDynamicUpdate &&
- buffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
-
- final Node node = new Node(nodeContents);
- node.mCachedAddress = nodeOrigin;
- reverseNodeMap.put(node.mCachedAddress, node);
- return node;
- }
-
- /**
- * Helper function to get the binary format version from the header.
- * @throws IOException
- */
- private static int getFormatVersion(final FusionDictionaryBufferInterface buffer)
- throws IOException {
- final int magic_v1 = buffer.readUnsignedShort();
- if (FormatSpec.VERSION_1_MAGIC_NUMBER == magic_v1) return buffer.readUnsignedByte();
- final int magic_v2 = (magic_v1 << 16) + buffer.readUnsignedShort();
- if (FormatSpec.VERSION_2_MAGIC_NUMBER == magic_v2) return buffer.readUnsignedShort();
- return FormatSpec.NOT_A_VERSION_NUMBER;
- }
-
- /**
- * Helper function to get and validate the binary format version.
- * @throws UnsupportedFormatException
- * @throws IOException
- */
- private static int checkFormatVersion(final FusionDictionaryBufferInterface buffer)
- throws IOException, UnsupportedFormatException {
- final int version = getFormatVersion(buffer);
- 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 header from a buffer.
- * @param buffer the buffer to read.
- * @throws IOException
- * @throws UnsupportedFormatException
- */
- public static FileHeader readHeader(final FusionDictionaryBufferInterface buffer)
- throws IOException, UnsupportedFormatException {
- final int version = checkFormatVersion(buffer);
- final int optionsFlags = buffer.readUnsignedShort();
-
- final HashMap<String, String> attributes = new HashMap<String, String>();
- final int headerSize;
- if (version < FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) {
- headerSize = buffer.position();
- } else {
- headerSize = buffer.readInt();
- populateOptions(buffer, headerSize, attributes);
- buffer.position(headerSize);
- }
-
- if (headerSize < 0) {
- throw new UnsupportedFormatException("header size can't be negative.");
- }
-
- 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)));
- return header;
- }
-
- /**
- * Reads options from a buffer and populate a map with their contents.
- *
- * The buffer is read at the current position, so the caller must take care the pointer
- * is in the right place before calling this.
- */
- public static void populateOptions(final FusionDictionaryBufferInterface buffer,
- final int headerSize, final HashMap<String, String> options) {
- while (buffer.position() < headerSize) {
- final String key = CharEncoding.readString(buffer);
- final String value = CharEncoding.readString(buffer);
- options.put(key, value);
- }
- }
-
- /**
- * 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 buffer the buffer to read.
- * @param dict an optional dictionary to add words to, or null.
- * @return the created (or merged) dictionary.
- */
- @UsedForTesting
- public static FusionDictionary readDictionaryBinary(
- final FusionDictionaryBufferInterface buffer, final FusionDictionary dict)
- throws IOException, UnsupportedFormatException {
- // clear cache
- wordCache.clear();
-
- // Read header
- final FileHeader header = readHeader(buffer);
-
- Map<Integer, Node> reverseNodeMapping = new TreeMap<Integer, Node>();
- Map<Integer, CharGroup> reverseGroupMapping = new TreeMap<Integer, CharGroup>();
- final Node root = readNode(buffer, header.mHeaderSize, reverseNodeMapping,
- reverseGroupMapping, header.mFormatOptions);
-
- FusionDictionary newDict = new FusionDictionary(root, header.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 ByteBufferWrapper(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
- }
- }
- }
- }
-
- /**
- * Calculate bigram frequency from compressed value
- *
- * @see #makeBigramFlags
- *
- * @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
new file mode 100644
index 000000000..3796a466c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/DictDecoder.java
@@ -0,0 +1,394 @@
+/*
+ * 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 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.HashMap;
+import java.util.TreeMap;
+
+/**
+ * The base class of binary dictionary decoders.
+ */
+public abstract class 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)));
+ return header;
+ }
+
+ /**
+ * Reads and returns the file header.
+ */
+ public abstract FileHeader readHeader() throws IOException, UnsupportedFormatException;
+
+ /**
+ * Reads PtNode from nodeAddress.
+ * @param ptNodePos the position of PtNode.
+ * @param formatOptions the format options.
+ * @return PtNodeInfo.
+ */
+ public abstract 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 abstract 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 {
+ if (!isDictBufferOpen()) {
+ openDictBuffer();
+ }
+ return BinaryDictIOUtils.getTerminalPosition(this, word);
+ }
+
+ /**
+ * 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 {
+ if (!isDictBufferOpen()) {
+ openDictBuffer();
+ }
+ BinaryDictIOUtils.readUnigramsAndBigramsBinary(this, words, frequencies, bigrams);
+ }
+
+ /**
+ * Sets the position of the buffer to the given value.
+ *
+ * @param newPos the new position
+ */
+ public abstract void setPosition(final int newPos);
+
+ /**
+ * Gets the position of the buffer.
+ *
+ * @return the position
+ */
+ public abstract int getPosition();
+
+ /**
+ * Reads and returns the PtNode count out of a buffer and forwards the pointer.
+ */
+ public abstract int readPtNodeCount();
+
+ /**
+ * Reads the forward link and advances the position.
+ *
+ * @return true if this method moves the file pointer, false otherwise.
+ */
+ public abstract boolean readAndFollowForwardLink();
+ public abstract boolean hasNextPtNodeArray();
+
+ /**
+ * Opens the dictionary file and makes DictBuffer.
+ */
+ @UsedForTesting
+ public abstract void openDictBuffer() throws FileNotFoundException, IOException;
+ @UsedForTesting
+ public abstract 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;
+ }
+ }
+
+ /**
+ * 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/DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java
new file mode 100644
index 000000000..ea5d492d8
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/DictEncoder.java
@@ -0,0 +1,38 @@
+/*
+ * 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/DynamicBinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
new file mode 100644
index 000000000..bf3d19101
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/DynamicBinaryDictIOUtils.java
@@ -0,0 +1,502 @@
+/*
+ * 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 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.
+ }
+
+ private static int markAsDeleted(final int flags) {
+ return (flags & (~FormatSpec.MASK_CHILDREN_ADDRESS_TYPE)) | FormatSpec.FLAG_IS_DELETED;
+ }
+
+ /**
+ * Delete the word from the binary file.
+ *
+ * @param dictDecoder the dict decoder.
+ * @param word the word we delete
+ * @throws IOException
+ * @throws UnsupportedFormatException
+ */
+ @UsedForTesting
+ public static void deleteWord(final Ver3DictDecoder dictDecoder, final String word)
+ throws IOException, UnsupportedFormatException {
+ final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
+ dictBuffer.position(0);
+ final FileHeader header = dictDecoder.readHeader();
+ final int wordPosition = dictDecoder.getTerminalPosition(word);
+ if (wordPosition == FormatSpec.NOT_VALID_WORD) return;
+
+ dictBuffer.position(wordPosition);
+ final int flags = dictBuffer.readUnsignedByte();
+ dictBuffer.position(wordPosition);
+ dictBuffer.put((byte)markAsDeleted(flags));
+ }
+
+ /**
+ * Update a parent address in a PtNode that is referred to by ptNodeOriginAddress.
+ *
+ * @param dictBuffer the DictBuffer to write.
+ * @param ptNodeOriginAddress the address of the PtNode.
+ * @param newParentAddress the absolute address of the parent.
+ * @param formatOptions file format options.
+ */
+ public static void updateParentAddress(final DictBuffer dictBuffer,
+ final int ptNodeOriginAddress, final int newParentAddress,
+ final FormatOptions formatOptions) {
+ 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 dictBuffer the DictBuffer 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.
+ */
+ public static void updateParentAddresses(final DictBuffer dictBuffer,
+ final int ptNodeOriginAddress, final int newParentAddress,
+ final FormatOptions formatOptions) {
+ final int originalPosition = dictBuffer.position();
+ dictBuffer.position(ptNodeOriginAddress);
+ do {
+ final int count = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
+ for (int i = 0; i < count; ++i) {
+ updateParentAddress(dictBuffer, dictBuffer.position(), newParentAddress,
+ formatOptions);
+ BinaryDictIOUtils.skipPtNode(dictBuffer, formatOptions);
+ }
+ final int forwardLinkAddress = dictBuffer.readUnsignedInt24();
+ dictBuffer.position(forwardLinkAddress);
+ } while (formatOptions.mSupportsDynamicUpdate
+ && dictBuffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
+ dictBuffer.position(originalPosition);
+ }
+
+ /**
+ * Update a children address in a PtNode that is addressed by ptNodeOriginAddress.
+ *
+ * @param dictBuffer the DictBuffer to write.
+ * @param ptNodeOriginAddress the address of the PtNode.
+ * @param newChildrenAddress the absolute address of the child.
+ * @param formatOptions file format options.
+ */
+ public static void updateChildrenAddress(final DictBuffer dictBuffer,
+ final int ptNodeOriginAddress, final int newChildrenAddress,
+ final FormatOptions formatOptions) {
+ final int originalPosition = dictBuffer.position();
+ dictBuffer.position(ptNodeOriginAddress);
+ final int flags = dictBuffer.readUnsignedByte();
+ final int parentAddress = 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 DictBuffer dictBuffer, final PtNodeInfo info,
+ final int nodeArrayOriginAddress, final int oldNodeAddress,
+ final FormatOptions formatOptions) throws IOException {
+ updateParentAddress(dictBuffer, 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(dictBuffer, nodeArrayOriginAddress, dictBuffer.limit(), formatOptions);
+ size += BinaryDictIOUtils.writeNodes(destination, new PtNodeInfo[] { info });
+ return size;
+ }
+
+ @SuppressWarnings("unused")
+ private static void updateForwardLink(final DictBuffer dictBuffer,
+ final int nodeArrayOriginAddress, final int newNodeArrayAddress,
+ final FormatOptions formatOptions) {
+ dictBuffer.position(nodeArrayOriginAddress);
+ int jumpCount = 0;
+ while (jumpCount++ < MAX_JUMPS) {
+ final int count = BinaryDictDecoderUtils.readPtNodeCount(dictBuffer);
+ for (int i = 0; i < count; ++i) {
+ BinaryDictIOUtils.skipPtNode(dictBuffer, 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 dictBuffer the DictBuffer representing the (constant-size) body of the file.
+ * @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 DictBuffer dictBuffer, 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, dictBuffer, newInfo, oldPtNodeArrayOrigin, oldPtNodeOrigin,
+ formatOptions);
+ return 1 + size + FormatSpec.FORWARD_LINK_ADDRESS_SIZE;
+ }
+
+ /**
+ * Insert a word into a binary dictionary.
+ *
+ * @param dictDecoder the dict decoder.
+ * @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 Ver3DictDecoder dictDecoder,
+ 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 = new ArrayList<PendingAttribute>();
+ final DictBuffer dictBuffer = dictDecoder.getDictBuffer();
+ if (bigramStrings != null) {
+ for (final WeightedString bigram : bigramStrings) {
+ int position = dictDecoder.getTerminalPosition(bigram.mWord);
+ if (position == FormatSpec.NOT_VALID_WORD) {
+ // TODO: figure out what is the correct thing to do here.
+ } else {
+ bigrams.add(new PendingAttribute(bigram.mFrequency, position));
+ }
+ }
+ }
+
+ 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 = dictDecoder.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 = dictDecoder.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,
+ dictBuffer, nodeOriginAddress, address, fileHeader.mFormatOptions);
+
+ final int[] characters2 = Arrays.copyOfRange(currentInfo.mCharacters, p,
+ currentInfo.mCharacters.length);
+ if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
+ updateParentAddresses(dictBuffer, 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, dictBuffer, nodeOriginAddress, address,
+ fileHeader.mFormatOptions);
+
+ final int[] suffixCharacters = Arrays.copyOfRange(
+ currentInfo.mCharacters, p, currentInfo.mCharacters.length);
+ if (currentInfo.mChildrenAddress != FormatSpec.NO_CHILDREN_ADDRESS) {
+ updateParentAddresses(dictBuffer, 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, dictBuffer, 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(dictBuffer, 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 e1e5e5500..849bff050 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -16,30 +16,68 @@
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;
+
/**
* Dictionary File Format Specification.
*/
public final class FormatSpec {
/*
- * Array of Node(FusionDictionary.Node) layout is as follows:
+ * File header layout is as follows:
+ *
+ * v |
+ * e | MAGIC_NUMBER + version of the file format, 2 bytes.
+ * r |
+ * sion
+ *
+ * o |
+ * p | not used 4 bits
+ * t | has bigrams ? 1 bit, 1 = yes, 0 = no : CONTAINS_BIGRAMS_FLAG
+ * i | FRENCH_LIGATURE_PROCESSING_FLAG
+ * o | supports dynamic updates ? 1 bit, 1 = yes, 0 = no : SUPPORTS_DYNAMIC_UPDATE
+ * n | GERMAN_UMLAUT_PROCESSING_FLAG
+ * f |
+ * lags
*
- * g |
- * r | the number of groups, 1 or 2 bytes.
- * o | 1 byte = bbbbbbbb match
- * u | case 1xxxxxxx => xxxxxxx << 8 + next byte
- * p | otherwise => bbbbbbbb
- * c |
- * ount
+ * h |
+ * e | size of the file header, 4bytes
+ * a | including the size of the magic number, the option flags and the header size
+ * d |
+ * ersize
+ *
+ * | attributes list
*
- * g |
- * r | sequence of groups,
- * o | the layout of each group is described below.
- * u |
- * ps
+ * attributes list is:
+ * <key> = | string of characters at the char format described below, with the terminator used
+ * | to signal the end of the string.
+ * <value> = | string of characters at the char format described below, with the terminator used
+ * | to signal the end of the string.
+ * if the size of already read < headersize, goto key.
+ *
+ */
+
+ /*
+ * Node array (FusionDictionary.PtNodeArray) layout is as follows:
+ *
+ * n |
+ * o | the number of PtNodes, 1 or 2 bytes.
+ * d | 1 byte = bbbbbbbb match
+ * e | case 1xxxxxxx => xxxxxxx << 8 + next byte
+ * c | otherwise => bbbbbbbb
+ * o |
+ * unt
+ *
+ * n |
+ * o | sequence of PtNodes,
+ * d | the layout of each PtNode is described below.
+ * e |
+ * s
*
* f |
* o | IF SUPPORTS_DYNAMIC_UPDATE (defined in the file header)
@@ -51,19 +89,19 @@ public final class FormatSpec {
* linkaddress
*/
- /* Node(CharGroup) layout is as follows:
+ /* Node (FusionDictionary.PtNode) layout is as follows:
* | IF !SUPPORTS_DYNAMIC_UPDATE
- * | addressType xx : mask with MASK_GROUP_ADDRESS_TYPE
- * | 2 bits, 00 = no children : FLAG_GROUP_ADDRESS_TYPE_NOADDRESS
- * f | 01 = 1 byte : FLAG_GROUP_ADDRESS_TYPE_ONEBYTE
- * l | 10 = 2 bytes : FLAG_GROUP_ADDRESS_TYPE_TWOBYTES
- * a | 11 = 3 bytes : FLAG_GROUP_ADDRESS_TYPE_THREEBYTES
+ * | 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_GROUP_ADDRESS_TYPE_THREEBYTES
- * | 01 = yes : FLAG_IS_MOVED
+ * 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
+ * | 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
@@ -77,11 +115,13 @@ public final class FormatSpec {
* 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 |
- * ddress
+ * 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.
+ * d |
+ * ress
*
* c | IF FLAG_HAS_MULTIPLE_CHARS
- * h | char, char, char, char n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers
+ * h | char, char, char, char n * (1 or 3 bytes) : use PtNodeInfo for i/o helpers
* a | end 1 byte, = 0
* r | ELSE
* s | char 1 or 3 bytes
@@ -92,17 +132,23 @@ public final class FormatSpec {
* e | frequency 1 byte
* q |
*
- * c | IF 00 = FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = addressType
- * h | // nothing
- * i | ELSIF 01 = FLAG_GROUP_ADDRESS_TYPE_ONEBYTE == addressType
- * l | children address, 1 byte
- * d | ELSIF 10 = FLAG_GROUP_ADDRESS_TYPE_TWOBYTES == addressType
- * r | children address, 2 bytes
- * e | ELSE // 11 = FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = addressType
- * n | children address, 3 bytes
- * A | END
- * d
- * dress
+ * 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.
*
* | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS
* | shortcut string list
@@ -121,42 +167,43 @@ public final class FormatSpec {
* characters which should never happen anyway (and still work, but take 3 bytes).
*
* bigram address list is:
- * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
- * | addressSign = 1 bit, : FLAG_ATTRIBUTE_OFFSET_NEGATIVE
+ * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT
+ * | addressSign = 1 bit, : FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE
* | 1 = must take -address, 0 = must take +address
- * | xx : mask with MASK_ATTRIBUTE_ADDRESS_TYPE
- * | addressFormat = 2 bits, 00 = unused : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
- * | 01 = 1 byte : FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE
- * | 10 = 2 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES
- * | 11 = 3 bytes : FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES
- * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY
- * <address> | IF (01 == FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE == addressFormat)
+ * | xx : mask with MASK_BIGRAM_ATTR_ADDRESS_TYPE
+ * | addressFormat = 2 bits, 00 = unused : FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE
+ * | 01 = 1 byte : FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE
+ * | 10 = 2 bytes : FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES
+ * | 11 = 3 bytes : FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES
+ * | 4 bits : frequency : mask with FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY
+ * <address> | IF (01 == FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE == addressFormat)
* | read 1 byte, add top 4 bits
- * | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat)
+ * | ELSIF (10 == FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES == addressFormat)
* | read 2 bytes, add top 4 bits
- * | ELSE // 11 == FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES == addressFormat
+ * | ELSE // 11 == FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES == addressFormat
* | read 3 bytes, add top 4 bits
* | END
- * | if (FLAG_ATTRIBUTE_OFFSET_NEGATIVE) then address = -address
- * if (FLAG_ATTRIBUTE_HAS_NEXT) goto bigram_and_shortcut_address_list_is
+ * | if (FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE) then address = -address
+ * if (FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT) goto bigram_and_shortcut_address_list_is
*
* shortcut string list is:
- * <byte size> = GROUP_SHORTCUT_LIST_SIZE_SIZE bytes, big-endian: size of the list, in bytes.
- * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
+ * <byte size> = PTNODE_SHORTCUT_LIST_SIZE_SIZE bytes, big-endian: size of the list, in bytes.
+ * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT
* | reserved = 3 bits, must be 0
- * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_FREQUENCY
+ * | 4 bits : frequency : mask with FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY
* <shortcut> = | string of characters at the char format described above, with the terminator
* | used to signal the end of the string.
- * if (FLAG_ATTRIBUTE_HAS_NEXT goto flags
+ * if (FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT goto flags
*/
- static final int VERSION_1_MAGIC_NUMBER = 0x78B1;
- public static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
- static final int MINIMUM_SUPPORTED_VERSION = 1;
- static final int MAXIMUM_SUPPORTED_VERSION = 3;
+ 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_HEADER_SIZE = 2;
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;
@@ -167,17 +214,17 @@ public final class FormatSpec {
// TODO: Make this value adaptative to content data, store it in the header, and
// use it in the reading code.
- static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH;
+ static final int MAX_WORD_LENGTH = Constants.DICTIONARY_MAX_WORD_LENGTH;
static final int PARENT_ADDRESS_SIZE = 3;
static final int FORWARD_LINK_ADDRESS_SIZE = 3;
// These flags are used only in the static dictionary.
- static final int MASK_GROUP_ADDRESS_TYPE = 0xC0;
- static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00;
- static final int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40;
- static final int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80;
- static final int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0;
+ static final int MASK_CHILDREN_ADDRESS_TYPE = 0xC0;
+ static final int FLAG_CHILDREN_ADDRESS_TYPE_NOADDRESS = 0x00;
+ static final int FLAG_CHILDREN_ADDRESS_TYPE_ONEBYTE = 0x40;
+ static final int FLAG_CHILDREN_ADDRESS_TYPE_TWOBYTES = 0x80;
+ static final int FLAG_CHILDREN_ADDRESS_TYPE_THREEBYTES = 0xC0;
static final int FLAG_HAS_MULTIPLE_CHARS = 0x20;
@@ -194,32 +241,42 @@ public final class FormatSpec {
static final int FLAG_IS_NOT_MOVED = 0x80 | FIXED_BIT_OF_DYNAMIC_UPDATE_MOVE;
static final int FLAG_IS_DELETED = 0x80;
- static final int FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
- static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
- static final int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
- static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
- static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
- static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
- static final int FLAG_ATTRIBUTE_FREQUENCY = 0x0F;
+ static final int FLAG_BIGRAM_SHORTCUT_ATTR_HAS_NEXT = 0x80;
+ static final int FLAG_BIGRAM_ATTR_OFFSET_NEGATIVE = 0x40;
+ static final int MASK_BIGRAM_ATTR_ADDRESS_TYPE = 0x30;
+ static final int FLAG_BIGRAM_ATTR_ADDRESS_TYPE_ONEBYTE = 0x10;
+ static final int FLAG_BIGRAM_ATTR_ADDRESS_TYPE_TWOBYTES = 0x20;
+ static final int FLAG_BIGRAM_ATTR_ADDRESS_TYPE_THREEBYTES = 0x30;
+ static final int FLAG_BIGRAM_SHORTCUT_ATTR_FREQUENCY = 0x0F;
+
+ static final int PTNODE_CHARACTERS_TERMINATOR = 0x1F;
- static final int GROUP_CHARACTERS_TERMINATOR = 0x1F;
+ static final int PTNODE_TERMINATOR_SIZE = 1;
+ static final int PTNODE_FLAGS_SIZE = 1;
+ static final int PTNODE_FREQUENCY_SIZE = 1;
+ static final int PTNODE_TERMINAL_ID_SIZE = 4;
+ static final int PTNODE_MAX_ADDRESS_SIZE = 3;
+ static final int PTNODE_ATTRIBUTE_FLAGS_SIZE = 1;
+ static final int PTNODE_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
+ static final int PTNODE_SHORTCUT_LIST_SIZE_SIZE = 2;
- static final int GROUP_TERMINATOR_SIZE = 1;
- static final int GROUP_FLAGS_SIZE = 1;
- static final int GROUP_FREQUENCY_SIZE = 1;
- static final int GROUP_MAX_ADDRESS_SIZE = 3;
- static final int GROUP_ATTRIBUTE_FLAGS_SIZE = 1;
- static final int GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
- static final int GROUP_SHORTCUT_LIST_SIZE_SIZE = 2;
+ // These values are used only by version 4 or later.
+ static final String TRIE_FILE_EXTENSION = ".trie";
+ static final String FREQ_FILE_EXTENSION = ".freq";
+ // tat = Terminal Address Table
+ static final String TERMINAL_ADDRESS_TABLE_FILE_EXTENSION = ".tat";
+ static final int FREQUENCY_AND_FLAGS_SIZE = 2;
+ static final int TERMINAL_ADDRESS_TABLE_ADDRESS_SIZE = 3;
static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
static final int NO_PARENT_ADDRESS = 0;
static final int NO_FORWARD_LINK_ADDRESS = 0;
static final int INVALID_CHARACTER = -1;
- static final int MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT = 0x7F; // 127
- static final int MAX_CHARGROUPS_IN_A_NODE = 0x7FFF; // 32767
- static final int MAX_BIGRAMS_IN_A_GROUP = 10000;
+ static final int MAX_PTNODES_FOR_ONE_BYTE_PTNODE_COUNT = 0x7F; // 127
+ static final int MAX_PTNODES_IN_A_PT_NODE_ARRAY = 0x7FFF; // 32767
+ static final int MAX_BIGRAMS_IN_A_PTNODE = 10000;
+ static final int MAX_SHORTCUT_LIST_SIZE_IN_A_PTNODE = 0xFFFF;
static final int MAX_TERMINAL_FREQUENCY = 255;
static final int MAX_BIGRAM_FREQUENCY = 15;
@@ -230,15 +287,26 @@ public final class FormatSpec {
static final int NOT_VALID_WORD = -99;
static final int SIGNED_CHILDREN_ADDRESS_SIZE = 3;
+ static final int UINT8_MAX = 0xFF;
+ static final int UINT16_MAX = 0xFFFF;
+ static final int UINT24_MAX = 0xFFFFFF;
+ static final int SINT24_MAX = 0x7FFFFF;
+ static final int MSB8 = 0x80;
+ static final int MSB24 = 0x800000;
+
/**
* Options about file format.
*/
public static final class FormatOptions {
public final int mVersion;
public final boolean mSupportsDynamicUpdate;
+ public final boolean mHasTerminalId;
+ @UsedForTesting
public FormatOptions(final int version) {
this(version, false);
}
+
+ @UsedForTesting
public FormatOptions(final int version, final boolean supportsDynamicUpdate) {
mVersion = version;
if (version < FIRST_VERSION_WITH_DYNAMIC_UPDATE && supportsDynamicUpdate) {
@@ -246,6 +314,7 @@ public final class FormatSpec {
+ FIRST_VERSION_WITH_DYNAMIC_UPDATE + " and ulterior.");
}
mSupportsDynamicUpdate = supportsDynamicUpdate;
+ mHasTerminalId = (version >= FIRST_VERSION_WITH_TERMINAL_ID);
}
}
@@ -256,6 +325,12 @@ public final class FormatSpec {
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";
+
private static final String DICTIONARY_VERSION_ATTRIBUTE = "version";
private static final String DICTIONARY_LOCALE_ATTRIBUTE = "locale";
private static final String DICTIONARY_ID_ATTRIBUTE = "dictionary";
@@ -290,6 +365,36 @@ public final class FormatSpec {
}
}
+ /**
+ * 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);
+ }
+ return null;
+ }
+
+ public static DictDecoder getDictDecoder(final File dictFile) {
+ return getDictDecoder(dictFile, DictDecoder.USE_READONLY_BYTEBUFFER);
+ }
+
private FormatSpec() {
// This utility class is not publicly instantiable.
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index 17d281518..be653feec 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -34,25 +34,33 @@ import java.util.LinkedList;
public final class FusionDictionary implements Iterable<Word> {
private static final boolean DBG = MakedictLog.DBG;
+ private static int CHARACTER_NOT_FOUND_INDEX = -1;
+
/**
- * A node of the dictionary, containing several CharGroups.
+ * A node array of the dictionary, containing several PtNodes.
*
- * A node is but an ordered array of CharGroups, which essentially contain all the
+ * 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 Node {
- ArrayList<CharGroup> mData;
+ public static final class PtNodeArray {
+ ArrayList<PtNode> mData;
// To help with binary generation
int mCachedSize = Integer.MIN_VALUE;
- int mCachedAddress = 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 Node() {
- mData = new ArrayList<CharGroup>();
+ public PtNodeArray() {
+ mData = new ArrayList<PtNode>();
}
- public Node(ArrayList<CharGroup> data) {
+ public PtNodeArray(ArrayList<PtNode> data) {
mData = data;
}
}
@@ -85,35 +93,44 @@ public final class FusionDictionary implements Iterable<Word> {
}
/**
- * A group of characters, with a frequency, shortcut targets, bigrams, and children.
+ * 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 CharGroup is what can
+ * 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 CharGroup essentially represents one or several characters in the middle
- * of the trie trie; as such, it can be a terminal, and it can have children.
- * In this in-memory representation, whether the CharGroup is a terminal or not is represented
+ * 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 CharGroup {
+ 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.
- Node mChildren;
+ int mTerminalId; // NOT_A_TERMINAL == mTerminalId indicates this is not a terminal.
+ PtNodeArray mChildren;
boolean mIsNotAWord; // Only a shortcut
boolean mIsBlacklistEntry;
- // The two following members to help with binary generation
- int mCachedSize;
- int mCachedAddress;
-
- public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
+ // 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;
@@ -121,9 +138,10 @@ public final class FusionDictionary implements Iterable<Word> {
mIsBlacklistEntry = isBlacklistEntry;
}
- public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
+ public PtNode(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
final ArrayList<WeightedString> bigrams, final int frequency,
- final boolean isNotAWord, final boolean isBlacklistEntry, final Node children) {
+ final boolean isNotAWord, final boolean isBlacklistEntry,
+ final PtNodeArray children) {
mChars = chars;
mFrequency = frequency;
mShortcutTargets = shortcutTargets;
@@ -133,13 +151,17 @@ public final class FusionDictionary implements Iterable<Word> {
mIsBlacklistEntry = isBlacklistEntry;
}
- public void addChild(CharGroup n) {
+ public void addChild(PtNode n) {
if (null == mChildren) {
- mChildren = new Node();
+ mChildren = new PtNodeArray();
}
mChildren.mData.add(n);
}
+ public int getTerminalId() {
+ return mTerminalId;
+ }
+
public boolean isTerminal() {
return NOT_A_TERMINAL != mFrequency;
}
@@ -230,7 +252,7 @@ public final class FusionDictionary implements Iterable<Word> {
}
/**
- * Updates the CharGroup with the given properties. Adds the shortcut and bigram lists to
+ * 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.
*/
@@ -330,10 +352,10 @@ public final class FusionDictionary implements Iterable<Word> {
}
public final DictionaryOptions mOptions;
- public final Node mRoot;
+ public final PtNodeArray mRootNodeArray;
- public FusionDictionary(final Node root, final DictionaryOptions options) {
- mRoot = root;
+ public FusionDictionary(final PtNodeArray rootNodeArray, final DictionaryOptions options) {
+ mRootNodeArray = rootNodeArray;
mOptions = options;
}
@@ -392,13 +414,13 @@ public final class FusionDictionary implements Iterable<Word> {
}
/**
- * Sanity check for a node.
+ * Sanity check for a PtNode array.
*
- * This method checks that all CharGroups in a node are ordered as expected.
+ * 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(Node node) {
- ArrayList<CharGroup> stack = node.mData;
+ 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];
@@ -417,18 +439,18 @@ public final class FusionDictionary implements Iterable<Word> {
* @param frequency the bigram frequency
*/
public void setBigram(final String word1, final String word2, final int frequency) {
- CharGroup charGroup = findWordInTree(mRoot, word1);
- if (charGroup != null) {
- final CharGroup charGroup2 = findWordInTree(mRoot, word2);
- if (charGroup2 == null) {
+ 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 chargroup for the first word may have moved by the above insertion,
+ // 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 charGroup.
- charGroup = findWordInTree(mRoot, word1);
+ // a cutting point until now. In this case, we need to refresh ptNode.
+ ptNode = findWordInTree(mRootNodeArray, word1);
}
- charGroup.addBigram(word2, frequency);
+ ptNode.addBigram(word2, frequency);
} else {
throw new RuntimeException("First word of bigram not found");
}
@@ -450,97 +472,96 @@ public final class FusionDictionary implements Iterable<Word> {
final ArrayList<WeightedString> shortcutTargets,
final boolean isNotAWord, final boolean isBlacklistEntry) {
assert(frequency >= 0 && frequency <= 255);
- if (word.length >= Constants.Dictionary.MAX_WORD_LENGTH) {
+ if (word.length >= Constants.DICTIONARY_MAX_WORD_LENGTH) {
MakedictLog.w("Ignoring a word that is too long: word.length = " + word.length);
return;
}
- Node currentNode = mRoot;
+ PtNodeArray currentNodeArray = mRootNodeArray;
int charIndex = 0;
- CharGroup currentGroup = null;
+ PtNode currentPtNode = null;
int differentCharIndex = 0; // Set by the loop to the index of the char that differs
- int nodeIndex = findIndexOfChar(mRoot, word[charIndex]);
- while (CHARACTER_NOT_FOUND != nodeIndex) {
- currentGroup = currentNode.mData.get(nodeIndex);
- differentCharIndex = compareArrays(currentGroup.mChars, word, charIndex);
+ 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 < currentGroup.mChars.length) break;
- if (null == currentGroup.mChildren) break;
- charIndex += currentGroup.mChars.length;
+ && differentCharIndex < currentPtNode.mChars.length) break;
+ if (null == currentPtNode.mChildren) break;
+ charIndex += currentPtNode.mChars.length;
if (charIndex >= word.length) break;
- currentNode = currentGroup.mChildren;
- nodeIndex = findIndexOfChar(currentNode, word[charIndex]);
+ currentNodeArray = currentPtNode.mChildren;
+ nodeIndex = findIndexOfChar(currentNodeArray, word[charIndex]);
}
- if (-1 == nodeIndex) {
+ if (CHARACTER_NOT_FOUND_INDEX == nodeIndex) {
// No node at this point to accept the word. Create one.
- final int insertionIndex = findInsertionIndex(currentNode, word[charIndex]);
- final CharGroup newGroup = new CharGroup(
- Arrays.copyOfRange(word, charIndex, word.length),
+ final int insertionIndex = findInsertionIndex(currentNodeArray, word[charIndex]);
+ final PtNode newPtNode = new PtNode(Arrays.copyOfRange(word, charIndex, word.length),
shortcutTargets, null /* bigrams */, frequency, isNotAWord, isBlacklistEntry);
- currentNode.mData.add(insertionIndex, newGroup);
- if (DBG) checkStack(currentNode);
+ currentNodeArray.mData.add(insertionIndex, newPtNode);
+ if (DBG) checkStack(currentNodeArray);
} else {
// There is a word with a common prefix.
- if (differentCharIndex == currentGroup.mChars.length) {
+ 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 CharNode was not a terminal,
+ // 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
- currentGroup.update(frequency, shortcutTargets, null, isNotAWord,
+ 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 CharGroup newNode = new CharGroup(
+ final PtNode newNode = new PtNode(
Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length),
shortcutTargets, null /* bigrams */, frequency, isNotAWord,
isBlacklistEntry);
- currentGroup.mChildren = new Node();
- currentGroup.mChildren.mData.add(newNode);
+ 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.
- currentGroup.update(frequency, shortcutTargets, null,
- currentGroup.mIsNotAWord && isNotAWord,
- currentGroup.mIsBlacklistEntry || isBlacklistEntry);
+ 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.
- Node newChildren = new Node();
- final CharGroup newOldWord = new CharGroup(
- Arrays.copyOfRange(currentGroup.mChars, differentCharIndex,
- currentGroup.mChars.length), currentGroup.mShortcutTargets,
- currentGroup.mBigrams, currentGroup.mFrequency,
- currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry,
- currentGroup.mChildren);
+ 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 CharGroup newParent;
+ final PtNode newParent;
if (charIndex + differentCharIndex >= word.length) {
- newParent = new CharGroup(
- Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+ newParent = new PtNode(
+ Arrays.copyOfRange(currentPtNode.mChars, 0, differentCharIndex),
shortcutTargets, null /* bigrams */, frequency,
isNotAWord, isBlacklistEntry, newChildren);
} else {
- newParent = new CharGroup(
- Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
+ newParent = new PtNode(
+ Arrays.copyOfRange(currentPtNode.mChars, 0, differentCharIndex),
null /* shortcutTargets */, null /* bigrams */, -1,
false /* isNotAWord */, false /* isBlacklistEntry */, newChildren);
- final CharGroup newWord = new CharGroup(Arrays.copyOfRange(word,
+ final PtNode newWord = new PtNode(Arrays.copyOfRange(word,
charIndex + differentCharIndex, word.length),
shortcutTargets, null /* bigrams */, frequency,
isNotAWord, isBlacklistEntry);
final int addIndex = word[charIndex + differentCharIndex]
- > currentGroup.mChars[differentCharIndex] ? 1 : 0;
+ > currentPtNode.mChars[differentCharIndex] ? 1 : 0;
newChildren.mData.add(addIndex, newWord);
}
- currentNode.mData.set(nodeIndex, newParent);
+ currentNodeArray.mData.set(nodeIndex, newParent);
}
- if (DBG) checkStack(currentNode);
+ if (DBG) checkStack(currentNodeArray);
}
}
}
@@ -562,7 +583,7 @@ public final class FusionDictionary implements Iterable<Word> {
* @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 compareArrays(final int[] src, final int[] dst, int dstOffset) {
+ 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) {
@@ -574,81 +595,81 @@ public final class FusionDictionary implements Iterable<Word> {
}
/**
- * Helper class that compares and sorts two chargroups according to their
+ * 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 CharGroupComparator implements java.util.Comparator<CharGroup> {
+ static private final class PtNodeComparator implements java.util.Comparator<PtNode> {
@Override
- public int compare(CharGroup c1, CharGroup c2) {
- if (c1.mChars[0] == c2.mChars[0]) return 0;
- return c1.mChars[0] < c2.mChars[0] ? -1 : 1;
+ 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 CharGroupComparator CHARGROUP_COMPARATOR = new CharGroupComparator();
+ final static private PtNodeComparator PTNODE_COMPARATOR = new PtNodeComparator();
/**
- * Finds the insertion index of a character within a node.
+ * Finds the insertion index of a character within a node array.
*/
- private static int findInsertionIndex(final Node node, int character) {
- final ArrayList<CharGroup> data = node.mData;
- final CharGroup reference = new CharGroup(new int[] { character },
+ 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, CHARGROUP_COMPARATOR);
+ int result = Collections.binarySearch(data, reference, PTNODE_COMPARATOR);
return result >= 0 ? result : -result - 1;
}
- private static int CHARACTER_NOT_FOUND = -1;
-
/**
- * Find the index of a char in a node, if it exists.
+ * Find the index of a char in a node array, if it exists.
*
- * @param node the node to search in.
+ * @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 = -1 else.
+ * @return the position of the character if it's there, or CHARACTER_NOT_FOUND_INDEX = -1 else.
*/
- private static int findIndexOfChar(final Node node, int character) {
- final int insertionIndex = findInsertionIndex(node, character);
- if (node.mData.size() <= insertionIndex) return CHARACTER_NOT_FOUND;
- return character == node.mData.get(insertionIndex).mChars[0] ? insertionIndex
- : CHARACTER_NOT_FOUND;
+ 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 CharGroup findWordInTree(Node node, final String string) {
+ public static PtNode findWordInTree(PtNodeArray nodeArray, final String string) {
int index = 0;
final StringBuilder checker = DBG ? new StringBuilder() : null;
final int[] codePoints = getCodePoints(string);
- CharGroup currentGroup;
+ PtNode currentPtNode;
do {
- int indexOfGroup = findIndexOfChar(node, codePoints[index]);
- if (CHARACTER_NOT_FOUND == indexOfGroup) return null;
- currentGroup = node.mData.get(indexOfGroup);
+ int indexOfGroup = findIndexOfChar(nodeArray, codePoints[index]);
+ if (CHARACTER_NOT_FOUND_INDEX == indexOfGroup) return null;
+ currentPtNode = nodeArray.mData.get(indexOfGroup);
- if (codePoints.length - index < currentGroup.mChars.length) return null;
+ if (codePoints.length - index < currentPtNode.mChars.length) return null;
int newIndex = index;
- while (newIndex < codePoints.length && newIndex - index < currentGroup.mChars.length) {
- if (currentGroup.mChars[newIndex - index] != codePoints[newIndex]) return null;
+ 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(currentGroup.mChars, 0, currentGroup.mChars.length));
+ if (DBG) {
+ checker.append(new String(currentPtNode.mChars, 0, currentPtNode.mChars.length));
+ }
if (index < codePoints.length) {
- node = currentGroup.mChildren;
+ nodeArray = currentPtNode.mChildren;
}
- } while (null != node && index < codePoints.length);
+ } while (null != nodeArray && index < codePoints.length);
if (index < codePoints.length) return null;
- if (!currentGroup.isTerminal()) return null;
+ if (!currentPtNode.isTerminal()) return null;
if (DBG && !string.equals(checker.toString())) return null;
- return currentGroup;
+ return currentPtNode;
}
/**
@@ -658,22 +679,22 @@ public final class FusionDictionary implements Iterable<Word> {
if (null == s || "".equals(s)) {
throw new RuntimeException("Can't search for a null or empty string");
}
- return null != findWordInTree(mRoot, s);
+ return null != findWordInTree(mRootNodeArray, s);
}
/**
- * Recursively count the number of character groups in a given branch of the trie.
+ * Recursively count the number of PtNodes in a given branch of the trie.
*
- * @param node the parent node.
- * @return the number of char groups in all the branch under this node.
+ * @param nodeArray the parent node.
+ * @return the number of PtNodes in all the branch under this node.
*/
- public static int countCharGroups(final Node node) {
- final int nodeSize = node.mData.size();
+ public static int countPtNodes(final PtNodeArray nodeArray) {
+ final int nodeSize = nodeArray.mData.size();
int size = nodeSize;
for (int i = nodeSize - 1; i >= 0; --i) {
- CharGroup group = node.mData.get(i);
- if (null != group.mChildren)
- size += countCharGroups(group.mChildren);
+ PtNode ptNode = nodeArray.mData.get(i);
+ if (null != ptNode.mChildren)
+ size += countPtNodes(ptNode.mChildren);
}
return size;
}
@@ -681,15 +702,15 @@ public final class FusionDictionary implements Iterable<Word> {
/**
* Recursively count the number of nodes in a given branch of the trie.
*
- * @param node the node to count.
+ * @param nodeArray the node array to count.
* @return the number of nodes in this branch.
*/
- public static int countNodes(final Node node) {
+ public static int countNodeArrays(final PtNodeArray nodeArray) {
int size = 1;
- for (int i = node.mData.size() - 1; i >= 0; --i) {
- CharGroup group = node.mData.get(i);
- if (null != group.mChildren)
- size += countNodes(group.mChildren);
+ 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;
}
@@ -697,12 +718,12 @@ public final class FusionDictionary implements Iterable<Word> {
// 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 Node node) {
- if (null == node) return false;
- for (int i = node.mData.size() - 1; i >= 0; --i) {
- CharGroup group = node.mData.get(i);
- if (null != group.mBigrams) return true;
- if (hasBigramsInternal(group.mChildren)) return true;
+ 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;
}
@@ -717,7 +738,7 @@ public final class FusionDictionary implements Iterable<Word> {
// find a more efficient way of doing this, without compromising too much on memory
// and ease of use.
public boolean hasBigrams() {
- return hasBigramsInternal(mRoot);
+ return hasBigramsInternal(mRootNodeArray);
}
// Historically, the tails of the words were going to be merged to save space.
@@ -735,16 +756,16 @@ public final class FusionDictionary implements Iterable<Word> {
MakedictLog.i("Do not merge tails");
return;
-// MakedictLog.i("Merging nodes. Number of nodes : " + countNodes(root));
-// MakedictLog.i("Number of groups : " + countCharGroups(root));
+// MakedictLog.i("Merging PtNodes. Number of PtNodes : " + countPtNodes(root));
+// MakedictLog.i("Number of PtNodes : " + countPtNodes(root));
//
-// final HashMap<String, ArrayList<Node>> repository =
-// new HashMap<String, ArrayList<Node>>();
+// 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<Node> a : repository.values()) {
+// for (ArrayList<PtNodeArray> a : repository.values()) {
// size += a.size();
// }
// MakedictLog.i("Number of nodes after merge : " + (1 + size));
@@ -752,58 +773,58 @@ public final class FusionDictionary implements Iterable<Word> {
}
// The following methods are used by the deactivated mergeTails()
-// private static boolean isEqual(Node a, Node b) {
+// 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) {
-// CharGroup aGroup = a.data.get(i);
-// CharGroup bGroup = b.data.get(i);
-// if (aGroup.frequency != bGroup.frequency) return false;
-// if (aGroup.alternates == null && bGroup.alternates != null) return false;
-// if (aGroup.alternates != null && !aGroup.equals(bGroup.alternates)) return false;
-// if (!Arrays.equals(aGroup.chars, bGroup.chars)) return false;
-// if (!isEqual(aGroup.children, bGroup.children)) return false;
+// 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<Node>> mergeTailsInner(
-// final HashMap<String, ArrayList<Node>> map, final Node node) {
-// final ArrayList<CharGroup> branches = node.data;
+// 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) {
-// CharGroup group = branches.get(i);
-// if (null != group.children) {
-// String pseudoHash = getPseudoHash(group.children);
-// ArrayList<Node> similarList = map.get(pseudoHash);
+// 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<Node>();
+// similarList = new ArrayList<PtNodeArray>();
// map.put(pseudoHash, similarList);
// }
// boolean merged = false;
-// for (Node similar : similarList) {
-// if (isEqual(group.children, similar)) {
-// group.children = similar;
+// for (PtNodeArray similar : similarList) {
+// if (isEqual(ptNode.children, similar)) {
+// ptNode.children = similar;
// merged = true;
// break;
// }
// }
// if (!merged) {
-// similarList.add(group.children);
+// similarList.add(ptNode.children);
// }
-// mergeTailsInner(map, group.children);
+// mergeTailsInner(map, ptNode.children);
// }
// }
// return map;
// }
-// private static String getPseudoHash(final Node node) {
+// private static String getPseudoHash(final PtNodeArray nodeArray) {
// StringBuilder s = new StringBuilder();
-// for (CharGroup g : node.data) {
-// s.append(g.frequency);
-// for (int ch : g.chars) {
+// for (PtNode ptNode : nodeArray.data) {
+// s.append(ptNode.frequency);
+// for (int ch : ptNode.chars) {
// s.append(Character.toChars(ch));
// }
// }
@@ -817,20 +838,20 @@ public final class FusionDictionary implements Iterable<Word> {
*/
public static final class DictionaryIterator implements Iterator<Word> {
private static final class Position {
- public Iterator<CharGroup> pos;
+ public Iterator<PtNode> pos;
public int length;
- public Position(ArrayList<CharGroup> groups) {
- pos = groups.iterator();
+ public Position(ArrayList<PtNode> ptNodes) {
+ pos = ptNodes.iterator();
length = 0;
}
}
final StringBuilder mCurrentString;
final LinkedList<Position> mPositions;
- public DictionaryIterator(ArrayList<CharGroup> root) {
+ public DictionaryIterator(ArrayList<PtNode> ptRoot) {
mCurrentString = new StringBuilder();
mPositions = new LinkedList<Position>();
- final Position rootPos = new Position(root);
+ final Position rootPos = new Position(ptRoot);
mPositions.add(rootPos);
}
@@ -851,20 +872,20 @@ public final class FusionDictionary implements Iterable<Word> {
do {
if (currentPos.pos.hasNext()) {
- final CharGroup currentGroup = currentPos.pos.next();
+ final PtNode currentPtNode = currentPos.pos.next();
currentPos.length = mCurrentString.length();
- for (int i : currentGroup.mChars) {
+ for (int i : currentPtNode.mChars) {
mCurrentString.append(Character.toChars(i));
}
- if (null != currentGroup.mChildren) {
- currentPos = new Position(currentGroup.mChildren.mData);
+ if (null != currentPtNode.mChildren) {
+ currentPos = new Position(currentPtNode.mChildren.mData);
currentPos.length = mCurrentString.length();
mPositions.addLast(currentPos);
}
- if (currentGroup.mFrequency >= 0) {
- return new Word(mCurrentString.toString(), currentGroup.mFrequency,
- currentGroup.mShortcutTargets, currentGroup.mBigrams,
- currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry);
+ if (currentPtNode.mFrequency >= 0) {
+ return new Word(mCurrentString.toString(), currentPtNode.mFrequency,
+ currentPtNode.mShortcutTargets, currentPtNode.mBigrams,
+ currentPtNode.mIsNotAWord, currentPtNode.mIsBlacklistEntry);
}
} else {
mPositions.removeLast();
@@ -889,6 +910,6 @@ public final class FusionDictionary implements Iterable<Word> {
*/
@Override
public Iterator<Word> iterator() {
- return new DictionaryIterator(mRoot.mData);
+ return new DictionaryIterator(mRootNodeArray.mData);
}
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java b/java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java
index b3617443e..188de7a0f 100644
--- a/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
+++ b/java/src/com/android/inputmethod/latin/makedict/PtNodeInfo.java
@@ -21,9 +21,9 @@ import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
import java.util.ArrayList;
/**
- * Raw char group info straight out of a file. This will contain numbers for addresses.
+ * Raw PtNode info straight out of a file. This will contain numbers for addresses.
*/
-public final class CharGroupInfo {
+public final class PtNodeInfo {
public final int mOriginalAddress;
public final int mEndAddress;
@@ -35,7 +35,7 @@ public final class CharGroupInfo {
public final ArrayList<WeightedString> mShortcutTargets;
public final ArrayList<PendingAttribute> mBigrams;
- public CharGroupInfo(final int originalAddress, final int endAddress, final int flags,
+ 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) {
diff --git a/java/src/com/android/inputmethod/latin/makedict/SparseTable.java b/java/src/com/android/inputmethod/latin/makedict/SparseTable.java
new file mode 100644
index 000000000..0b9cf91d2
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/SparseTable.java
@@ -0,0 +1,150 @@
+/*
+ * 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 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 mContentsTable 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<Integer> mContentTable;
+
+ private final int mBlockSize;
+ public static final int NOT_EXIST = -1;
+
+ @UsedForTesting
+ public SparseTable(final int initialCapacity, final int blockSize) {
+ mBlockSize = blockSize;
+ final int lookupTableSize = initialCapacity / mBlockSize
+ + (initialCapacity % mBlockSize > 0 ? 1 : 0);
+ mLookupTable = new ArrayList<Integer>(Collections.nCopies(lookupTableSize, NOT_EXIST));
+ mContentTable = new ArrayList<Integer>();
+ }
+
+ @UsedForTesting
+ public SparseTable(final int[] lookupTable, final int[] contentTable, final int blockSize) {
+ mBlockSize = blockSize;
+ mLookupTable = new ArrayList<Integer>(lookupTable.length);
+ for (int i = 0; i < lookupTable.length; ++i) {
+ mLookupTable.add(lookupTable[i]);
+ }
+ mContentTable = new ArrayList<Integer>(contentTable.length);
+ for (int i = 0; i < contentTable.length; ++i) {
+ mContentTable.add(contentTable[i]);
+ }
+ }
+
+ /**
+ * 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 void convertByteArrayToIntegerArray(final byte[] byteArray,
+ final ArrayList<Integer> integerArray) {
+ 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);
+ }
+ }
+
+ @UsedForTesting
+ public SparseTable(final byte[] lookupTable, final byte[] contentTable, final int blockSize) {
+ mBlockSize = blockSize;
+ mLookupTable = new ArrayList<Integer>(lookupTable.length / 4);
+ mContentTable = new ArrayList<Integer>(contentTable.length / 4);
+ convertByteArrayToIntegerArray(lookupTable, mLookupTable);
+ convertByteArrayToIntegerArray(contentTable, mContentTable);
+ }
+
+ @UsedForTesting
+ public int get(final int index) {
+ if (index < 0 || index / mBlockSize >= mLookupTable.size()
+ || mLookupTable.get(index / mBlockSize) == NOT_EXIST) {
+ return NOT_EXIST;
+ }
+ return mContentTable.get(mLookupTable.get(index / mBlockSize) + (index % mBlockSize));
+ }
+
+ @UsedForTesting
+ public void set(final int index, final int value) {
+ if (mLookupTable.get(index / mBlockSize) == NOT_EXIST) {
+ mLookupTable.set(index / mBlockSize, mContentTable.size());
+ for (int i = 0; i < mBlockSize; ++i) {
+ mContentTable.add(NOT_EXIST);
+ }
+ }
+ mContentTable.set(mLookupTable.get(index / mBlockSize) + (index % mBlockSize), value);
+ }
+
+ public void remove(final int index) {
+ set(index, NOT_EXIST);
+ }
+
+ @UsedForTesting
+ public int size() {
+ return mLookupTable.size() * mBlockSize;
+ }
+
+ @UsedForTesting
+ /* package */ int getContentTableSize() {
+ return mContentTable.size();
+ }
+
+ @UsedForTesting
+ /* package */ int getLookupTableSize() {
+ return mLookupTable.size();
+ }
+
+ public boolean contains(final int index) {
+ return get(index) != NOT_EXIST;
+ }
+
+ @UsedForTesting
+ public void write(final OutputStream lookupOutStream, final OutputStream contentOutStream)
+ throws IOException {
+ for (final int index : mLookupTable) {
+ BinaryDictEncoderUtils.writeUIntToStream(lookupOutStream, index, 4);
+ }
+
+ for (final int index : mContentTable) {
+ BinaryDictEncoderUtils.writeUIntToStream(contentOutStream, index, 4);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java
new file mode 100644
index 000000000..848277cd4
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictDecoder.java
@@ -0,0 +1,234 @@
+/*
+ * 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 DictDecoder {
+ 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 DictDecoder.PtNodeReader {
+ private static int readFrequency(final DictBuffer dictBuffer) {
+ return dictBuffer.readUnsignedByte();
+ }
+ }
+
+ private final File mDictionaryBinaryFile;
+ private final DictionaryBufferFactory mBufferFactory;
+ private 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) {
+ MakedictLog.d("too many 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;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java
new file mode 100644
index 000000000..76f0f4052
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver3DictEncoder.java
@@ -0,0 +1,255 @@
+/*
+ * 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);
+ }
+ mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition, ptNodeCount,
+ countSize);
+ }
+
+ private void writePtNodeFlags(final PtNode ptNode, final int parentAddress,
+ final FormatOptions formatOptions) {
+ final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions);
+ mPosition = BinaryDictEncoderUtils.writeUIntToBuffer(mBuffer, mPosition,
+ BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mPosition, 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, parentPosition, 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/Ver4DictDecoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
new file mode 100644
index 000000000..4c8ff8ea4
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
@@ -0,0 +1,266 @@
+/*
+ * 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 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 DictDecoder {
+ 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 final File mDictDirectory;
+ private final DictionaryBufferFactory mBufferFactory;
+ private DictBuffer mDictBuffer;
+ private DictBuffer mFrequencyBuffer;
+ private DictBuffer mTerminalAddressTableBuffer;
+
+ @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 {
+ throw new RuntimeException("Unsupported kind of file : " + fileType);
+ }
+ }
+
+ @Override
+ public void openDictBuffer() throws FileNotFoundException, IOException {
+ final String filename = mDictDirectory.getName();
+ mDictBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_TRIE));
+ mFrequencyBuffer = mBufferFactory.getDictionaryBuffer(getFile(FILETYPE_FREQUENCY));
+ mTerminalAddressTableBuffer = mBufferFactory.getDictionaryBuffer(
+ getFile(FILETYPE_TERMINAL_ADDRESS_TABLE));
+ }
+
+ @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;
+ }
+
+ protected static class PtNodeReader extends DictDecoder.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();
+ }
+ }
+
+ // 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;
+ 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) {
+ MakedictLog.d("too many bigrams in a node.");
+ }
+ } 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;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
new file mode 100644
index 000000000..4fb89671f
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
@@ -0,0 +1,294 @@
+/*
+/*
+ * 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 mTerminalAddressTableOutStream;
+
+ @UsedForTesting
+ public Ver4DictEncoder(final File dictPlacedDir) {
+ mDictPlacedDir = dictPlacedDir;
+ }
+
+ private void openStreams(final FormatOptions formatOptions, final DictionaryOptions dictOptions)
+ throws FileNotFoundException, IOException {
+ final FileHeader header = new FileHeader(0, dictOptions, formatOptions);
+ final String filename = header.getId() + "." + header.getVersion();
+ final File mDictDir = new File(mDictPlacedDir, filename);
+ final File trieFile = new File(mDictDir, filename + FormatSpec.TRIE_FILE_EXTENSION);
+ final File freqFile = new File(mDictDir, filename + FormatSpec.FREQ_FILE_EXTENSION);
+ final File terminalAddressTableFile = new File(mDictDir,
+ filename + FormatSpec.TERMINAL_ADDRESS_TABLE_FILE_EXTENSION);
+ if (!mDictDir.isDirectory()) {
+ if (mDictDir.exists()) mDictDir.delete();
+ mDictDir.mkdirs();
+ }
+ if (!trieFile.exists()) trieFile.createNewFile();
+ if (!freqFile.exists()) freqFile.createNewFile();
+ if (!terminalAddressTableFile.exists()) terminalAddressTableFile.createNewFile();
+ mTrieOutStream = new FileOutputStream(trieFile);
+ mFreqOutStream = new FileOutputStream(freqFile);
+ mTerminalAddressTableOutStream = new FileOutputStream(terminalAddressTableFile);
+ }
+
+ private void close() throws IOException {
+ try {
+ if (mTrieOutStream != null) {
+ mTrieOutStream.close();
+ }
+ if (mFreqOutStream != null) {
+ mFreqOutStream.close();
+ }
+ if (mTerminalAddressTableOutStream != null) {
+ mTerminalAddressTableOutStream.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);
+
+ 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);
+ }
+ mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos, ptNodeCount,
+ countSize);
+ }
+
+ private void writePtNodeFlags(final PtNode ptNode, final int parentAddress,
+ final FormatOptions formatOptions) {
+ final int childrenPos = BinaryDictEncoderUtils.getChildrenPosition(ptNode, formatOptions);
+ mTriePos = BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, mTriePos,
+ BinaryDictEncoderUtils.makePtNodeFlags(ptNode, mTriePos, 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 writeShortcuts(ArrayList<WeightedString> shortcuts) {
+ if (null == shortcuts || shortcuts.isEmpty()) return;
+
+ final int indexOfShortcutByteSize = mTriePos;
+ mTriePos += 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);
+ mTrieBuf[mTriePos++] = (byte)shortcutFlags;
+ final int shortcutShift = CharEncoding.writeString(mTrieBuf, mTriePos,
+ target.mWord);
+ mTriePos += shortcutShift;
+ }
+ final int shortcutByteSize = mTriePos - indexOfShortcutByteSize;
+ if (shortcutByteSize > FormatSpec.MAX_SHORTCUT_LIST_SIZE_IN_A_PTNODE) {
+ throw new RuntimeException("Shortcut list too large : " + shortcutByteSize);
+ }
+ BinaryDictEncoderUtils.writeUIntToBuffer(mTrieBuf, indexOfShortcutByteSize,
+ shortcutByteSize, FormatSpec.PTNODE_SHORTCUT_LIST_SIZE_SIZE);
+ }
+
+ private void writeBigrams(ArrayList<WeightedString> bigrams, 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
+ - (mTriePos + FormatSpec.PTNODE_ATTRIBUTE_FLAGS_SIZE);
+ int bigramFlags = BinaryDictEncoderUtils.makeBigramFlags(bigramIterator.hasNext(),
+ offset, bigram.mFrequency, unigramFrequencyForThisWord, bigram.mWord);
+ mTrieBuf[mTriePos++] = (byte) bigramFlags;
+ mTriePos += BinaryDictEncoderUtils.writeChildrenPosition(mTrieBuf,
+ mTriePos, Math.abs(offset));
+ }
+ }
+
+ @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, parentPosition, formatOptions);
+ writeParentPosition(parentPosition, ptNode, formatOptions);
+ writeCharacters(ptNode.mChars, ptNode.hasSeveralChars());
+ if (ptNode.isTerminal()) {
+ writeTerminalId(ptNode.mTerminalId);
+ }
+ writeChildrenPosition(ptNode, formatOptions);
+ writeShortcuts(ptNode.mShortcutTargets);
+ writeBigrams(ptNode.mBigrams, dict);
+ }
+
+ 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);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
new file mode 100644
index 000000000..66517a800
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/DecayingExpandableBinaryDictionaryBase.java
@@ -0,0 +1,231 @@
+/*
+ * 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 android.content.SharedPreferences;
+import android.util.Log;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Constants;
+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 java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class is a base class of a dictionary that supports decaying for the personalized language
+ * model.
+ */
+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;
+
+ /** Any pair being typed or picked */
+ public static final int FREQUENCY_FOR_TYPED = 2;
+
+ /** Locale for which this user history dictionary is storing words */
+ private final String mLocale;
+
+ private final String mFileName;
+
+ 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);
+ mLocale = locale;
+ mFileName = fileName;
+ mPrefs = sp;
+ if (mLocale != null && mLocale.length() > 1) {
+ asyncLoadDictionaryToMemory();
+ reloadDictionaryIfRequired();
+ }
+ }
+
+ @Override
+ public void close() {
+ if (!ExpandableBinaryDictionary.ENABLE_BINARY_DICTIONARY_DYNAMIC_UPDATE) {
+ closeBinaryDictionary();
+ }
+ // Flush pending writes.
+ // TODO: Remove after this class become to use a dynamic binary dictionary.
+ asyncFlashAllBinaryDictionary();
+ Settings.writeLastUserHistoryWriteTime(mPrefs, mLocale);
+ }
+
+ @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);
+ return attributeMap;
+ }
+
+ @Override
+ protected boolean hasContentChanged() {
+ return false;
+ }
+
+ @Override
+ protected boolean needsToReloadBeforeWriting() {
+ return false;
+ }
+
+ /**
+ * Return whether the passed charsequence is in the dictionary.
+ */
+ @Override
+ public boolean isValidWord(final String word) {
+ // Words included only in the user history should be treated as not in dictionary words.
+ return false;
+ }
+
+ /**
+ * Pair will be added to the personalization prediction dictionary.
+ *
+ * The first word may be null. That means we don't know the context, in other words,
+ * it's only a unigram. The first word may also be an empty string : this means start
+ * context, as in beginning of a sentence for example.
+ * The second word may not be null (a NullPointerException would be thrown).
+ */
+ public void addToPersonalizationPredictionDictionary(
+ final String word0, final String word1, final boolean isValid) {
+ if (word1.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH ||
+ (word0 != null && word0.length() >= Constants.DICTIONARY_MAX_WORD_LENGTH)) {
+ return;
+ }
+ addWordDynamically(word1, null /* the "shortcut" parameter is null */, FREQUENCY_FOR_TYPED,
+ false /* isNotAWord */);
+ // Do not insert a word as a bigram of itself
+ if (word1.equals(word0)) {
+ return;
+ }
+ if (null != word0) {
+ addBigramDynamically(word0, word1, FREQUENCY_FOR_TYPED, isValid);
+ }
+ }
+
+ 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) {
+ if (DBG_SAVE_RESTORE) {
+ Log.d(TAG, "load unigram: " + word + "," + frequency);
+ }
+ addWord(word, shortcutTarget, frequency, 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);
+ }
+
+ public void clearAndFlushDictionary() {
+ // Clear the node structure on memory
+ clear();
+ // Then flush the cleared state of the dictionary on disk.
+ asyncFlashAllBinaryDictionary();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
new file mode 100644
index 000000000..0af028a9e
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/DynamicPersonalizationDictionaryWriter.java
@@ -0,0 +1,183 @@
+/*
+ * 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;
+
+// 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.
+ */
+ @Override
+ public void addUnigramWord(final String word, final String shortcutTarget, final int frequency,
+ final boolean isNotAWord) {
+ if (mBigramList.size() > mMaxHistoryBigrams * 2) {
+ // Too many entries: just stop adding new vocabrary and wait next refresh.
+ return;
+ }
+ mExpandableDictionary.addWord(word, shortcutTarget, frequency);
+ 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 vocabrary 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)
+ 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 isInDictionaryForTests(final String word) {
+ // TODO: Use native method to determine whether the word is in dictionary or not
+ return mBigramList.containsKey(word);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
new file mode 100644
index 000000000..f257165cb
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionary.java
@@ -0,0 +1,73 @@
+/*
+ * 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 com.android.inputmethod.latin.utils.CollectionUtils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.util.ArrayList;
+
+/**
+ * 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();
+
+ /** Locale for which this user history dictionary is storing words */
+ private final String mLocale;
+
+ 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();
+ }
+
+ @Override
+ protected void loadDictionaryAsync() {
+ // TODO: Implement
+ }
+
+ @Override
+ protected boolean hasContentChanged() {
+ 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/PersonalizationDictionarySessionRegister.java
new file mode 100644
index 000000000..c1833ff14
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegister.java
@@ -0,0 +1,37 @@
+/*
+ * 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 android.content.res.Configuration;
+
+public class PersonalizationDictionarySessionRegister {
+ public static void init(Context context) {
+ }
+
+ public static void onConfigurationChanged(final Context context, final Configuration conf) {
+ }
+
+ public static void onUpdateData(Context context, String type) {
+ }
+
+ public static void onRemoveData(Context context, String type) {
+ }
+
+ public static void onDestroy(Context context) {
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
new file mode 100644
index 000000000..c616a296c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdateSession.java
@@ -0,0 +1,129 @@
+/*
+ * 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.addToPersonalizationPredictionDictionary(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.addToPersonalizationPredictionDictionary(
+ 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
new file mode 100644
index 000000000..5f702ee3f
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
@@ -0,0 +1,122 @@
+/*
+ * 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.utils.CollectionUtils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import java.lang.ref.SoftReference;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class PersonalizationHelper {
+ private static final String TAG = PersonalizationHelper.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private static final ConcurrentHashMap<String, SoftReference<UserHistoryPredictionDictionary>>
+ 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 UserHistoryPredictionDictionary getUserHistoryPredictionDictionary(
+ final Context context, final String locale, final SharedPreferences sp) {
+ synchronized (sLangUserHistoryDictCache) {
+ if (sLangUserHistoryDictCache.containsKey(locale)) {
+ final SoftReference<UserHistoryPredictionDictionary> ref =
+ sLangUserHistoryDictCache.get(locale);
+ final UserHistoryPredictionDictionary dict = ref == null ? null : ref.get();
+ if (dict != null) {
+ if (DEBUG) {
+ Log.w(TAG, "Use cached UserHistoryPredictionDictionary for " + locale);
+ }
+ dict.reloadDictionaryIfRequired();
+ return dict;
+ }
+ }
+ final UserHistoryPredictionDictionary dict =
+ new UserHistoryPredictionDictionary(context, locale, sp);
+ sLangUserHistoryDictCache.put(
+ locale, new SoftReference<UserHistoryPredictionDictionary>(dict));
+ return dict;
+ }
+ }
+
+ 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 PersonalizationDictionary getPersonalizationDictionary(
+ final Context context, final String locale, final SharedPreferences sp) {
+ synchronized (sLangPersonalizationDictCache) {
+ if (sLangPersonalizationDictCache.containsKey(locale)) {
+ final SoftReference<PersonalizationDictionary> ref =
+ sLangPersonalizationDictCache.get(locale);
+ final PersonalizationDictionary dict = ref == null ? null : ref.get();
+ if (dict != null) {
+ if (DEBUG) {
+ Log.w(TAG, "Use cached PersonalizationDictCache for " + locale);
+ }
+ return dict;
+ }
+ }
+ final PersonalizationDictionary dict =
+ new PersonalizationDictionary(context, locale, sp);
+ sLangPersonalizationDictCache.put(
+ locale, 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);
+ }
+ return dict;
+ }
+ }
+ final PersonalizationPredictionDictionary dict =
+ new PersonalizationPredictionDictionary(context, locale, sp);
+ sLangPersonalizationPredictionDictCache.put(
+ locale, new SoftReference<PersonalizationPredictionDictionary>(dict));
+ return dict;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java
new file mode 100644
index 000000000..432954453
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationPredictionDictionary.java
@@ -0,0 +1,37 @@
+/*
+ * 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/UserHistoryDictionaryBigramList.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java
index 316f09603..55a90ee51 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
+++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryBigramList.java
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+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;
@@ -44,6 +45,7 @@ public final class UserHistoryDictionaryBigramList {
/**
* Called when the user typed a word.
*/
+ @UsedForTesting
public void addBigram(String word1, String word2) {
addBigram(word1, word2, FORGETTING_CURVE_INITIAL_VALUE);
}
@@ -52,7 +54,7 @@ public final class UserHistoryDictionaryBigramList {
* Called when loaded from the SQL DB.
*/
public void addBigram(String word1, String word2, byte fcValue) {
- if (UserHistoryDictionary.DBG_SAVE_RESTORE) {
+ if (DecayingExpandableBinaryDictionaryBase.DBG_SAVE_RESTORE) {
Log.d(TAG, "--- add bigram: " + word1 + ", " + word2 + ", " + fcValue);
}
final HashMap<String, Byte> map;
@@ -72,7 +74,7 @@ public final class UserHistoryDictionaryBigramList {
* Called when inserted to the SQL DB.
*/
public void updateBigram(String word1, String word2, byte fcValue) {
- if (UserHistoryDictionary.DBG_SAVE_RESTORE) {
+ if (DecayingExpandableBinaryDictionaryBase.DBG_SAVE_RESTORE) {
Log.d(TAG, "--- update bigram: " + word1 + ", " + word2 + ", " + fcValue);
}
final HashMap<String, Byte> map;
@@ -95,6 +97,10 @@ public final class UserHistoryDictionaryBigramList {
return mBigramMap.isEmpty();
}
+ public boolean containsKey(String word) {
+ return mBigramMap.containsKey(word);
+ }
+
public Set<String> keySet() {
return mBigramMap.keySet();
}
diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java
new file mode 100644
index 000000000..38e308a4e
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryPredictionDictionary.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * 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 UserHistoryPredictionDictionary extends DecayingExpandableBinaryDictionaryBase {
+ /* package for tests */ static final String NAME =
+ UserHistoryPredictionDictionary.class.getSimpleName();
+ /* package */ UserHistoryPredictionDictionary(final Context context, final String locale,
+ final SharedPreferences sp) {
+ super(context, locale, sp, Dictionary.TYPE_USER_HISTORY, 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/AdditionalFeaturesSettingUtils.java b/java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
index 0fdaea50c..6543003e8 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalFeaturesSettingUtils.java
+++ b/java/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
-
-import com.android.inputmethodcommon.InputMethodSettingsFragment;
+package com.android.inputmethod.latin.settings;
import android.content.Context;
import android.content.SharedPreferences;
+import com.android.inputmethodcommon.InputMethodSettingsFragment;
+
/**
* Utility class for managing additional features settings.
*/
@@ -40,8 +40,4 @@ public class AdditionalFeaturesSettingUtils {
final SharedPreferences prefs, final int[] additionalFeaturesPreferences) {
// do nothing.
}
-
- public static int[] getAdditionalNativeSuggestOptions() {
- return Settings.getInstance().getCurrent().mAdditionalFeaturesSettingValues;
- }
}
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java
index ff5e33949..4bf524cbb 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
+++ b/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.settings;
import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE;
@@ -44,6 +44,13 @@ import android.widget.Spinner;
import android.widget.SpinnerAdapter;
import android.widget.Toast;
+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.IntentUtils;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
+
import java.util.ArrayList;
import java.util.TreeSet;
@@ -71,7 +78,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment {
public SubtypeLocaleItem(final String localeString) {
this(localeString,
- SubtypeLocale.getSubtypeLocaleDisplayNameInSystemLocale(localeString));
+ SubtypeLocaleUtils.getSubtypeLocaleDisplayNameInSystemLocale(localeString));
}
@Override
@@ -102,7 +109,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment {
if (DEBUG_SUBTYPE_ID) {
android.util.Log.d(TAG, String.format("%-6s 0x%08x %11d %s",
subtype.getLocale(), subtype.hashCode(), subtype.hashCode(),
- SubtypeLocale.getSubtypeDisplayNameInSystemLocale(subtype)));
+ SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype)));
}
if (subtype.containsExtraValueKey(ASCII_CAPABLE)) {
items.add(createItem(context, subtype.getLocale()));
@@ -114,7 +121,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment {
public static SubtypeLocaleItem createItem(final Context context,
final String localeString) {
- if (localeString.equals(SubtypeLocale.NO_LANGUAGE)) {
+ if (localeString.equals(SubtypeLocaleUtils.NO_LANGUAGE)) {
final String displayName = context.getString(R.string.subtype_no_language);
return new SubtypeLocaleItem(localeString, displayName);
} else {
@@ -125,8 +132,8 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment {
static final class KeyboardLayoutSetItem extends Pair<String, String> {
public KeyboardLayoutSetItem(final InputMethodSubtype subtype) {
- super(SubtypeLocale.getKeyboardLayoutSetName(subtype),
- SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype));
+ super(SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype),
+ SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype));
}
@Override
@@ -141,10 +148,10 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// TODO: Should filter out already existing combinations of locale and layout.
- for (final String layout : SubtypeLocale.getPredefinedKeyboardLayoutSet()) {
+ for (final String layout : SubtypeLocaleUtils.getPredefinedKeyboardLayoutSet()) {
// This is a dummy subtype with NO_LANGUAGE, only for display.
- final InputMethodSubtype subtype = AdditionalSubtype.createAdditionalSubtype(
- SubtypeLocale.NO_LANGUAGE, layout, null);
+ final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype(
+ SubtypeLocaleUtils.NO_LANGUAGE, layout, null);
add(new KeyboardLayoutSetItem(subtype));
}
}
@@ -205,11 +212,11 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment {
setKey(KEY_NEW_SUBTYPE);
} else {
final String displayName =
- SubtypeLocale.getSubtypeDisplayNameInSystemLocale(subtype);
+ SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype);
setTitle(displayName);
setDialogTitle(displayName);
setKey(KEY_PREFIX + subtype.getLocale() + "_"
- + SubtypeLocale.getKeyboardLayoutSetName(subtype));
+ + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype));
}
}
@@ -279,7 +286,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment {
(SubtypeLocaleItem) mSubtypeLocaleSpinner.getSelectedItem();
final KeyboardLayoutSetItem layout =
(KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem();
- final InputMethodSubtype subtype = AdditionalSubtype.createAdditionalSubtype(
+ final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype(
locale.first, layout.first, ASCII_CAPABLE);
setSubtype(subtype);
notifyChanged();
@@ -497,13 +504,13 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment {
final Context context = getActivity();
final Resources res = context.getResources();
final String message = res.getString(R.string.custom_input_style_already_exists,
- SubtypeLocale.getSubtypeDisplayNameInSystemLocale(subtype));
+ SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype));
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}
private InputMethodSubtype findDuplicatedSubtype(final InputMethodSubtype subtype) {
final String localeString = subtype.getLocale();
- final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
+ final String keyboardLayoutSetName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
return mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
localeString, keyboardLayoutSetName);
}
@@ -536,7 +543,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment {
final PreferenceGroup group = getPreferenceScreen();
group.removeAll();
final InputMethodSubtype[] subtypesArray =
- AdditionalSubtype.createAdditionalSubtypesArray(prefSubtypes);
+ AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtypes);
for (final InputMethodSubtype subtype : subtypesArray) {
final SubtypePreference pref = new SubtypePreference(
context, subtype, mSubtypeProxy);
@@ -565,7 +572,7 @@ public final class AdditionalSubtypeSettings extends PreferenceFragment {
super.onPause();
final String oldSubtypes = Settings.readPrefAdditionalSubtypes(mPrefs, getResources());
final InputMethodSubtype[] subtypes = getSubtypes();
- final String prefSubtypes = AdditionalSubtype.createPrefSubtypes(subtypes);
+ final String prefSubtypes = AdditionalSubtypeUtils.createPrefSubtypes(subtypes);
if (prefSubtypes.equals(oldSubtypes)) {
return;
}
diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java
index 5969a63de..1b592b565 100644
--- a/java/src/com/android/inputmethod/latin/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/settings/DebugSettings.java
@@ -14,31 +14,33 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.settings;
-import android.content.Context;
import android.content.SharedPreferences;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.Process;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
-import android.util.Log;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
-import com.android.inputmethod.research.ResearchLogger;
+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;
public final class DebugSettings extends PreferenceFragment
implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = DebugSettings.class.getSimpleName();
public static final String PREF_DEBUG_MODE = "debug_mode";
public static final String PREF_FORCE_NON_DISTINCT_MULTITOUCH = "force_non_distinct_multitouch";
public static final String PREF_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";
private static final String PREF_READ_EXTERNAL_DICTIONARY = "read_external_dictionary";
private static final boolean SHOW_STATISTICS_LOGGING = false;
@@ -68,7 +70,7 @@ public final class DebugSettings extends PreferenceFragment
}
}
- PreferenceScreen readExternalDictionary =
+ final PreferenceScreen readExternalDictionary =
(PreferenceScreen) findPreference(PREF_READ_EXTERNAL_DICTIONARY);
if (null != readExternalDictionary) {
readExternalDictionary.setOnPreferenceClickListener(
@@ -113,6 +115,8 @@ public final class DebugSettings extends PreferenceFragment
} else if (key.equals(PREF_FORCE_NON_DISTINCT_MULTITOUCH)
|| key.equals(KeyboardSwitcher.PREF_KEYBOARD_LAYOUT)) {
mServiceNeedsRestart = true;
+ } else if (key.equals(PREF_USE_ONLY_PERSONALIZATION_DICTIONARY_FOR_DEBUG)) {
+ mServiceNeedsRestart = true;
}
}
@@ -122,7 +126,7 @@ public final class DebugSettings extends PreferenceFragment
}
boolean isDebugMode = mDebugMode.isChecked();
final String version = getResources().getString(
- R.string.version_text, Utils.getVersionName(getActivity()));
+ R.string.version_text, ApplicationUtils.getVersionName(getActivity()));
if (!isDebugMode) {
mDebugMode.setTitle(version);
mDebugMode.setSummary("");
diff --git a/java/src/com/android/inputmethod/latin/DebugSettingsActivity.java b/java/src/com/android/inputmethod/latin/settings/DebugSettingsActivity.java
index e1b5a802e..b499c26b6 100644
--- a/java/src/com/android/inputmethod/latin/DebugSettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/settings/DebugSettingsActivity.java
@@ -14,12 +14,14 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.settings;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceActivity;
+import com.android.inputmethod.latin.R;
+
public final class DebugSettingsActivity extends PreferenceActivity {
private static final String DEFAULT_FRAGMENT = DebugSettings.class.getName();
diff --git a/java/src/com/android/inputmethod/latin/NativeSuggestOptions.java b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
index 291551301..cd726c969 100644
--- a/java/src/com/android/inputmethod/latin/NativeSuggestOptions.java
+++ b/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.settings;
public class NativeSuggestOptions {
// Need to update suggest_options.h when you add, remove or reorder options.
@@ -34,6 +34,9 @@ public class NativeSuggestOptions {
}
public void setAdditionalFeaturesOptions(final int[] additionalOptions) {
+ if (additionalOptions == null) {
+ return;
+ }
for (int i = 0; i < additionalOptions.length; i++) {
setIntegerOption(OPTIONS_SIZE + i, additionalOptions[i]);
}
diff --git a/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java b/java/src/com/android/inputmethod/latin/settings/SeekBarDialogPreference.java
index 3ea9fedd7..802574aa8 100644
--- a/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
+++ b/java/src/com/android/inputmethod/latin/settings/SeekBarDialogPreference.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.settings;
import android.app.AlertDialog;
import android.content.Context;
@@ -26,6 +26,8 @@ import android.view.View;
import android.widget.SeekBar;
import android.widget.TextView;
+import com.android.inputmethod.latin.R;
+
public final class SeekBarDialogPreference extends DialogPreference
implements SeekBar.OnSeekBarChangeListener {
public interface ValueProxy {
@@ -33,10 +35,10 @@ public final class SeekBarDialogPreference extends DialogPreference
public int readDefaultValue(final String key);
public void writeValue(final int value, final String key);
public void writeDefaultValue(final String key);
+ public String getValueText(final int value);
public void feedbackValue(final int value);
}
- private final int mValueFormatResId;
private final int mMaxValue;
private final int mMinValue;
private final int mStepValue;
@@ -50,7 +52,6 @@ public final class SeekBarDialogPreference extends DialogPreference
super(context, attrs);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.SeekBarDialogPreference, 0, 0);
- mValueFormatResId = a.getResourceId(R.styleable.SeekBarDialogPreference_valueFormatText, 0);
mMaxValue = a.getInt(R.styleable.SeekBarDialogPreference_maxValue, 0);
mMinValue = a.getInt(R.styleable.SeekBarDialogPreference_minValue, 0);
mStepValue = a.getInt(R.styleable.SeekBarDialogPreference_stepValue, 0);
@@ -60,15 +61,8 @@ public final class SeekBarDialogPreference extends DialogPreference
public void setInterface(final ValueProxy proxy) {
mValueProxy = proxy;
- setSummary(getValueText(clipValue(proxy.readValue(getKey()))));
- }
-
- private String getValueText(final int value) {
- if (mValueFormatResId == 0) {
- return Integer.toString(value);
- } else {
- return getContext().getString(mValueFormatResId, value);
- }
+ final int value = mValueProxy.readValue(getKey());
+ setSummary(mValueProxy.getValueText(value));
}
@Override
@@ -101,16 +95,11 @@ public final class SeekBarDialogPreference extends DialogPreference
return clipValue(getValueFromProgress(progress));
}
- private void setValue(final int value, final boolean fromUser) {
- mValueView.setText(getValueText(value));
- if (!fromUser) {
- mSeekBar.setProgress(getProgressFromValue(value));
- }
- }
-
@Override
protected void onBindDialogView(final View view) {
- setValue(clipValue(mValueProxy.readValue(getKey())), false /* fromUser */);
+ final int value = mValueProxy.readValue(getKey());
+ mValueView.setText(mValueProxy.getValueText(value));
+ mSeekBar.setProgress(getProgressFromValue(clipValue(value)));
}
@Override
@@ -125,13 +114,15 @@ public final class SeekBarDialogPreference extends DialogPreference
super.onClick(dialog, which);
final String key = getKey();
if (which == DialogInterface.BUTTON_NEUTRAL) {
- setValue(clipValue(mValueProxy.readDefaultValue(key)), false /* fromUser */);
+ final int value = mValueProxy.readDefaultValue(key);
+ setSummary(mValueProxy.getValueText(value));
mValueProxy.writeDefaultValue(key);
return;
}
if (which == DialogInterface.BUTTON_POSITIVE) {
- setSummary(mValueView.getText());
- mValueProxy.writeValue(getClippedValueFromProgress(mSeekBar.getProgress()), key);
+ final int value = getClippedValueFromProgress(mSeekBar.getProgress());
+ setSummary(mValueProxy.getValueText(value));
+ mValueProxy.writeValue(value, key);
return;
}
}
@@ -139,7 +130,11 @@ public final class SeekBarDialogPreference extends DialogPreference
@Override
public void onProgressChanged(final SeekBar seekBar, final int progress,
final boolean fromUser) {
- setValue(getClippedValueFromProgress(progress), fromUser);
+ final int value = getClippedValueFromProgress(progress);
+ mValueView.setText(mValueProxy.getValueText(value));
+ if (!fromUser) {
+ mSeekBar.setProgress(getProgressFromValue(value));
+ }
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java
index a6149c6ec..1a0fecc62 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/settings/Settings.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.settings;
import android.content.Context;
import android.content.SharedPreferences;
@@ -23,10 +23,18 @@ import android.content.res.Resources;
import android.preference.PreferenceManager;
import android.util.Log;
-import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
+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.Locale;
+import java.util.concurrent.locks.ReentrantLock;
public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = Settings.class.getSimpleName();
@@ -36,7 +44,9 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_VIBRATE_ON = "vibrate_on";
public static final String PREF_SOUND_ON = "sound_on";
public static final String PREF_POPUP_ON = "popup_on";
- public static final String PREF_VOICE_MODE = "voice_mode";
+ // PREF_VOICE_MODE_OBSOLETE is obsolete. Use PREF_VOICE_INPUT_KEY instead.
+ public static final String PREF_VOICE_MODE_OBSOLETE = "voice_mode";
+ public static final String PREF_VOICE_INPUT_KEY = "pref_voice_input_key";
public static final String PREF_CORRECTION_SETTINGS = "correction_settings";
public static final String PREF_EDIT_PERSONAL_DICTIONARY = "edit_personal_dictionary";
public static final String PREF_CONFIGURE_DICTIONARIES_KEY = "configure_dictionaries_key";
@@ -71,6 +81,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT =
"pref_gesture_floating_preview_text";
public static final String PREF_SHOW_SETUP_WIZARD_ICON = "pref_show_setup_wizard_icon";
+ public static final String PREF_PHRASE_GESTURE_ENABLED = "pref_gesture_space_aware";
public static final String PREF_INPUT_LANGUAGE = "input_language";
public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
@@ -82,13 +93,19 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
"pref_suppress_language_switch_key";
+ private static final String PREF_LAST_USED_PERSONALIZATION_TOKEN =
+ "pref_last_used_personalization_token";
public static final String PREF_SEND_FEEDBACK = "send_feedback";
public static final String PREF_ABOUT_KEYBOARD = "about_keyboard";
+ // Emoji
+ public static final String PREF_EMOJI_RECENT_KEYS = "emoji_recent_keys";
+ public static final String PREF_EMOJI_CATEGORY_LAST_TYPED_ID = "emoji_category_last_typed_id";
+
private Resources mRes;
private SharedPreferences mPrefs;
- private Locale mCurrentLocale;
private SettingsValues mSettingsValues;
+ private final ReentrantLock mSettingsValuesLock = new ReentrantLock();
private static final Settings sInstance = new Settings();
@@ -116,25 +133,34 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
@Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
- if (mSettingsValues == null) {
- // TODO: Introduce a static function to register this class and ensure that
- // loadSettings must be called before "onSharedPreferenceChanged" is called.
- Log.w(TAG, "onSharedPreferenceChanged called before loadSettings.");
- return;
+ mSettingsValuesLock.lock();
+ try {
+ if (mSettingsValues == null) {
+ // TODO: Introduce a static function to register this class and ensure that
+ // loadSettings must be called before "onSharedPreferenceChanged" is called.
+ Log.w(TAG, "onSharedPreferenceChanged called before loadSettings.");
+ return;
+ }
+ loadSettings(mSettingsValues.mLocale, mSettingsValues.mInputAttributes);
+ } finally {
+ mSettingsValuesLock.unlock();
}
- loadSettings(mCurrentLocale, mSettingsValues.mInputAttributes);
}
public void loadSettings(final Locale locale, final InputAttributes inputAttributes) {
- mCurrentLocale = locale;
- final SharedPreferences prefs = mPrefs;
- final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
- @Override
- protected SettingsValues job(final Resources res) {
- return new SettingsValues(prefs, res, inputAttributes);
- }
- };
- mSettingsValues = job.runInLocale(mRes, locale);
+ mSettingsValuesLock.lock();
+ 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);
+ }
+ };
+ mSettingsValues = job.runInLocale(mRes, locale);
+ } finally {
+ mSettingsValuesLock.unlock();
+ }
}
// TODO: Remove this method and add proxy method to SettingsValues.
@@ -150,8 +176,8 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
return mSettingsValues.mWordSeparators;
}
- public Locale getCurrentLocale() {
- return mCurrentLocale;
+ public boolean isWordSeparator(final int code) {
+ return mSettingsValues.isWordSeparator(code);
}
public boolean getBlockPotentiallyOffensive() {
@@ -195,6 +221,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
&& prefs.getBoolean(Settings.PREF_GESTURE_INPUT, true);
}
+ public static boolean readPhraseGestureEnabled(final SharedPreferences prefs,
+ final Resources res) {
+ return prefs.getBoolean(Settings.PREF_PHRASE_GESTURE_ENABLED,
+ 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);
@@ -231,7 +263,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static String readPrefAdditionalSubtypes(final SharedPreferences prefs,
final Resources res) {
- final String predefinedPrefSubtypes = AdditionalSubtype.createPrefSubtypes(
+ final String predefinedPrefSubtypes = AdditionalSubtypeUtils.createPrefSubtypes(
res.getStringArray(R.array.predefined_subtypes));
return prefs.getString(PREF_CUSTOM_INPUT_STYLES, predefinedPrefSubtypes);
}
@@ -320,4 +352,46 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static boolean isInternal(final SharedPreferences prefs) {
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();
+ }
+
+ public byte[] readLastUsedPersonalizationToken() {
+ final String tokenStr = mPrefs.getString(PREF_LAST_USED_PERSONALIZATION_TOKEN, null);
+ return StringUtils.hexStringToByteArray(tokenStr);
+ }
+
+ public static void writeEmojiRecentKeys(final SharedPreferences prefs, String str) {
+ prefs.edit().putString(PREF_EMOJI_RECENT_KEYS, str).apply();
+ }
+
+ public static String readEmojiRecentKeys(final SharedPreferences prefs) {
+ return prefs.getString(PREF_EMOJI_RECENT_KEYS, "");
+ }
+
+ public static void writeEmojiCategoryLastTypedId(
+ final SharedPreferences prefs, final int category, final int id) {
+ final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + category;
+ prefs.edit().putInt(key, id).apply();
+ }
+
+ public static int readEmojiCategoryLastTypedId(
+ final SharedPreferences prefs, final int category) {
+ final String key = PREF_EMOJI_CATEGORY_LAST_TYPED_ID + category;
+ return prefs.getInt(key, 0);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/SettingsActivity.java b/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java
index 37ac2e35c..6c3818651 100644
--- a/java/src/com/android/inputmethod/latin/SettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsActivity.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.settings;
import android.content.Intent;
import android.preference.PreferenceActivity;
diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
index 7225cd6bf..cb7dda655 100644
--- a/java/src/com/android/inputmethod/latin/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.settings;
import android.app.Activity;
import android.app.backup.BackupManager;
@@ -25,6 +25,7 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.media.AudioManager;
+import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
@@ -32,22 +33,35 @@ import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
+import android.util.Log;
import android.view.inputmethod.InputMethodSubtype;
-import java.util.TreeSet;
-
import com.android.inputmethod.dictionarypack.DictionarySettingsActivity;
+import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeSwitcher;
import com.android.inputmethod.latin.define.ProductionFlag;
import com.android.inputmethod.latin.setup.LauncherIconVisibilityManager;
import com.android.inputmethod.latin.userdictionary.UserDictionaryList;
import com.android.inputmethod.latin.userdictionary.UserDictionarySettings;
+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;
+
public final class SettingsFragment extends InputMethodSettingsFragment
implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final boolean DBG_USE_INTERNAL_USER_DICTIONARY_SETTINGS = false;
+ private static final String TAG = SettingsFragment.class.getSimpleName();
+ private static final boolean DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS = false;
+ private static final boolean USE_INTERNAL_PERSONAL_DICTIONARY_SETTIGS =
+ DBG_USE_INTERNAL_PERSONAL_DICTIONARY_SETTINGS
+ || Build.VERSION.SDK_INT <= 18 /* Build.VERSION.JELLY_BEAN_MR2 */;
- private ListPreference mVoicePreference;
+ private CheckBoxPreference mVoiceInputKeyPreference;
private ListPreference mShowCorrectionSuggestionsPreference;
private ListPreference mAutoCorrectionThresholdPreference;
private ListPreference mKeyPreviewPopupDismissDelay;
@@ -80,7 +94,7 @@ public final class SettingsFragment extends InputMethodSettingsFragment
final PreferenceScreen preferenceScreen = getPreferenceScreen();
if (preferenceScreen != null) {
preferenceScreen.setTitle(
- Utils.getAcitivityTitleResId(getActivity(), SettingsActivity.class));
+ ApplicationUtils.getAcitivityTitleResId(getActivity(), SettingsActivity.class));
}
final Resources res = getResources();
@@ -90,10 +104,11 @@ public final class SettingsFragment extends InputMethodSettingsFragment
// singleton and utility classes may not have been initialized. We have to call
// initialization method of these classes here. See {@link LatinIME#onCreate()}.
SubtypeSwitcher.init(context);
- SubtypeLocale.init(context);
+ SubtypeLocaleUtils.init(context);
AudioAndHapticFeedbackManager.init(context);
- mVoicePreference = (ListPreference) findPreference(Settings.PREF_VOICE_MODE);
+ mVoiceInputKeyPreference =
+ (CheckBoxPreference) findPreference(Settings.PREF_VOICE_INPUT_KEY);
mShowCorrectionSuggestionsPreference =
(ListPreference) findPreference(Settings.PREF_SHOW_SUGGESTIONS_SETTING);
final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
@@ -128,7 +143,12 @@ public final class SettingsFragment extends InputMethodSettingsFragment
feedbackSettings.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(final Preference pref) {
- FeedbackUtils.showFeedbackForm(getActivity());
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ // Use development-only feedback mechanism
+ ResearchLogger.getInstance().presentFeedbackDialogFromSettings();
+ } else {
+ FeedbackUtils.showFeedbackForm(getActivity());
+ }
return true;
}
});
@@ -139,11 +159,15 @@ public final class SettingsFragment extends InputMethodSettingsFragment
miscSettings.removePreference(aboutSettings);
}
}
+ if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+ // The about screen contains items that may be confusing in development-only versions.
+ miscSettings.removePreference(aboutSettings);
+ }
final boolean showVoiceKeyOption = res.getBoolean(
R.bool.config_enable_show_voice_key_option);
if (!showVoiceKeyOption) {
- generalSettings.removePreference(mVoicePreference);
+ generalSettings.removePreference(mVoiceInputKeyPreference);
}
final PreferenceGroup advancedSettings =
@@ -190,27 +214,26 @@ public final class SettingsFragment extends InputMethodSettingsFragment
final Intent intent = dictionaryLink.getIntent();
intent.setClassName(context.getPackageName(), DictionarySettingsActivity.class.getName());
final int number = context.getPackageManager().queryIntentActivities(intent, 0).size();
- // TODO: The development-only-diagnostic version is not supported by the Dictionary Pack
- // Service yet
- if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS || 0 >= number) {
+ if (0 >= number) {
textCorrectionGroup.removePreference(dictionaryLink);
}
final Preference editPersonalDictionary =
findPreference(Settings.PREF_EDIT_PERSONAL_DICTIONARY);
final Intent editPersonalDictionaryIntent = editPersonalDictionary.getIntent();
- final ResolveInfo ri = context.getPackageManager().resolveActivity(
- editPersonalDictionaryIntent, PackageManager.MATCH_DEFAULT_ONLY);
- if (DBG_USE_INTERNAL_USER_DICTIONARY_SETTINGS || ri == null) {
- updateUserDictionaryPreference(editPersonalDictionary);
+ final ResolveInfo ri = USE_INTERNAL_PERSONAL_DICTIONARY_SETTIGS ? null
+ : context.getPackageManager().resolveActivity(
+ editPersonalDictionaryIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (ri == null) {
+ overwriteUserDictionaryPreference(editPersonalDictionary);
}
if (!Settings.readFromBuildConfigIfGestureInputEnabled(res)) {
removePreference(Settings.PREF_GESTURE_SETTINGS, getPreferenceScreen());
- } else {
- AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(context, this);
}
+ AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(context, this);
+
setupKeyLongpressTimeoutSettings(prefs, res);
setupKeypressVibrationDurationSettings(prefs, res);
setupKeypressSoundVolumeSettings(prefs, res);
@@ -221,10 +244,8 @@ public final class SettingsFragment extends InputMethodSettingsFragment
public void onResume() {
super.onResume();
final boolean isShortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled();
- if (isShortcutImeEnabled) {
- updateVoiceModeSummary();
- } else {
- getPreferenceScreen().removePreference(mVoicePreference);
+ if (!isShortcutImeEnabled) {
+ getPreferenceScreen().removePreference(mVoiceInputKeyPreference);
}
final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
final CheckBoxPreference showSetupWizardIcon =
@@ -246,7 +267,14 @@ public final class SettingsFragment extends InputMethodSettingsFragment
@Override
public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
- (new BackupManager(getActivity())).dataChanged();
+ final Activity activity = getActivity();
+ if (activity == null) {
+ // TODO: Introduce a static function to register this class and ensure that
+ // onCreate must be called before "onSharedPreferenceChanged" is called.
+ Log.w(TAG, "onSharedPreferenceChanged called before activity starts.");
+ return;
+ }
+ (new BackupManager(activity)).dataChanged();
final Resources res = getResources();
if (key.equals(Settings.PREF_POPUP_ON)) {
setPreferenceEnabled(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
@@ -258,7 +286,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment
LauncherIconVisibilityManager.updateSetupWizardIconVisibility(getActivity());
}
ensureConsistencyOfAutoCorrectionSettings();
- updateVoiceModeSummary();
updateShowCorrectionSuggestionsSummary();
updateKeyPreviewPopupDelaySummary();
refreshEnablingsOfKeypressSoundAndVibrationSettings(prefs, getResources());
@@ -285,11 +312,11 @@ public final class SettingsFragment extends InputMethodSettingsFragment
final Resources res = getResources();
final String prefSubtype = Settings.readPrefAdditionalSubtypes(prefs, res);
final InputMethodSubtype[] subtypes =
- AdditionalSubtype.createAdditionalSubtypesArray(prefSubtype);
+ AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtype);
final StringBuilder styles = new StringBuilder();
for (final InputMethodSubtype subtype : subtypes) {
if (styles.length() > 0) styles.append(", ");
- styles.append(SubtypeLocale.getSubtypeDisplayNameInSystemLocale(subtype));
+ styles.append(SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype));
}
customInputStyles.setSummary(styles);
}
@@ -301,12 +328,6 @@ public final class SettingsFragment extends InputMethodSettingsFragment
lp.setSummary(entries[lp.findIndexOfValue(lp.getValue())]);
}
- private void updateVoiceModeSummary() {
- mVoicePreference.setSummary(
- getResources().getStringArray(R.array.voice_input_modes_summary)
- [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
- }
-
private void refreshEnablingsOfKeypressSoundAndVibrationSettings(
final SharedPreferences sp, final Resources res) {
setPreferenceEnabled(Settings.PREF_VIBRATION_DURATION_SETTINGS,
@@ -347,6 +368,14 @@ public final class SettingsFragment extends InputMethodSettingsFragment
public void feedbackValue(final int value) {
AudioAndHapticFeedbackManager.getInstance().vibrate(value);
}
+
+ @Override
+ public String getValueText(final int value) {
+ if (value < 0) {
+ return res.getString(R.string.settings_system_default);
+ }
+ return res.getString(R.string.abbreviation_unit_milliseconds, value);
+ }
});
}
@@ -379,6 +408,11 @@ public final class SettingsFragment extends InputMethodSettingsFragment
}
@Override
+ public String getValueText(final int value) {
+ return res.getString(R.string.abbreviation_unit_milliseconds, value);
+ }
+
+ @Override
public void feedbackValue(final int value) {}
});
}
@@ -422,6 +456,14 @@ public final class SettingsFragment extends InputMethodSettingsFragment
}
@Override
+ public String getValueText(final int value) {
+ if (value < 0) {
+ return res.getString(R.string.settings_system_default);
+ }
+ return Integer.toString(value);
+ }
+
+ @Override
public void feedbackValue(final int value) {
am.playSoundEffect(
AudioManager.FX_KEYPRESS_STANDARD, getValueFromPercentage(value));
@@ -429,7 +471,7 @@ public final class SettingsFragment extends InputMethodSettingsFragment
});
}
- private void updateUserDictionaryPreference(Preference userDictionaryPreference) {
+ private void overwriteUserDictionaryPreference(Preference userDictionaryPreference) {
final Activity activity = getActivity();
final TreeSet<String> localeList = UserDictionaryList.getUserDictionaryLocalesSet(activity);
if (null == localeList) {
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index 09102447f..ee322e91b 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.settings;
import android.content.SharedPreferences;
import android.content.res.Configuration;
@@ -22,15 +22,26 @@ 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.Dictionary;
+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 java.util.Arrays;
+import java.util.Locale;
/**
* When you call the constructor of this class, you may want to change the current system locale by
- * using {@link LocaleUtils.RunInLocale}.
+ * using {@link com.android.inputmethod.latin.utils.RunInLocale}.
*/
public final class SettingsValues {
private static final String TAG = SettingsValues.class.getSimpleName();
@@ -46,13 +57,14 @@ public final class SettingsValues {
public final SuggestedWords mSuggestPuncList;
public final String mWordSeparators;
public final CharSequence mHintToSaveText;
+ public final boolean mCurrentLanguageHasSpaces;
// 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 String mVoiceMode;
+ private final boolean mShowsVoiceInputKey;
public final boolean mIncludesOtherImesInLanguageSwitchList;
public final boolean mShowsLanguageSwitchKey;
public final boolean mUseContactsDict;
@@ -61,10 +73,12 @@ public final class SettingsValues {
// Use bigrams to predict the next word when there is no input for it yet
public final boolean mBigramPredictionEnabled;
public final boolean mGestureInputEnabled;
- public final boolean mGesturePreviewTrailEnabled;
+ public final boolean mGestureTrailEnabled;
public final boolean mGestureFloatingPreviewTextEnabled;
public final boolean mSlidingKeyInputPreviewEnabled;
+ public final boolean mPhraseGestureEnabled;
public final int mKeyLongpressTimeout;
+ public final Locale mLocale;
// From the input box
public final InputAttributes mInputAttributes;
@@ -77,8 +91,8 @@ public final class SettingsValues {
public final float mAutoCorrectionThreshold;
public final boolean mCorrectionEnabled;
public final int mSuggestionVisibility;
- private final boolean mVoiceKeyEnabled;
- private final boolean mVoiceKeyOnMain;
+ public final boolean mBoostPersonalizationDictionaryForDebug;
+ public final boolean mUseOnlyPersonalizationDictionaryForDebug;
// Setting values for additional features
public final int[] mAdditionalFeaturesSettingValues =
@@ -87,8 +101,9 @@ public final class SettingsValues {
// Debug settings
public final boolean mIsInternal;
- public SettingsValues(final SharedPreferences prefs, final Resources res,
+ public SettingsValues(final SharedPreferences prefs, final Locale locale, final Resources res,
final InputAttributes inputAttributes) {
+ mLocale = locale;
// Get the resources
mDelayUpdateOldSuggestions = res.getInteger(R.integer.config_delay_update_old_suggestions);
mSymbolsPrecededBySpace =
@@ -105,6 +120,7 @@ public final class SettingsValues {
mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
mWordSeparators = res.getString(R.string.symbols_word_separators);
mHintToSaveText = res.getText(R.string.hint_add_to_dictionary);
+ mCurrentLanguageHasSpaces = res.getBoolean(R.bool.current_language_has_spaces);
// Store the input attributes
if (null == inputAttributes) {
@@ -120,9 +136,7 @@ public final class SettingsValues {
mKeyPreviewPopupOn = Settings.readKeyPreviewPopupEnabled(prefs, res);
mSlidingKeyInputPreviewEnabled = prefs.getBoolean(
Settings.PREF_SLIDING_KEY_INPUT_PREVIEW, true);
- final String voiceModeMain = res.getString(R.string.voice_mode_main);
- final String voiceModeOff = res.getString(R.string.voice_mode_off);
- mVoiceMode = prefs.getString(Settings.PREF_VOICE_MODE, voiceModeMain);
+ mShowsVoiceInputKey = needsToShowVoiceInputKey(prefs, res);
final String autoCorrectionThresholdRawValue = prefs.getString(
Settings.PREF_AUTO_CORRECTION_THRESHOLD,
res.getString(R.string.auto_correction_threshold_mode_index_modest));
@@ -142,12 +156,11 @@ public final class SettingsValues {
mKeyPreviewPopupDismissDelay = Settings.readKeyPreviewPopupDismissDelay(prefs, res);
mAutoCorrectionThreshold = readAutoCorrectionThreshold(res,
autoCorrectionThresholdRawValue);
- mVoiceKeyEnabled = mVoiceMode != null && !mVoiceMode.equals(voiceModeOff);
- mVoiceKeyOnMain = mVoiceMode != null && mVoiceMode.equals(voiceModeMain);
mGestureInputEnabled = Settings.readGestureInputEnabled(prefs, res);
- mGesturePreviewTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true);
+ mGestureTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true);
mGestureFloatingPreviewTextEnabled = prefs.getBoolean(
Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true);
+ mPhraseGestureEnabled = Settings.readPhraseGestureEnabled(prefs, res);
mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
final String showSuggestionsSetting = prefs.getString(
Settings.PREF_SHOW_SUGGESTIONS_SETTING,
@@ -156,6 +169,61 @@ 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);
+ 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);
}
public boolean isApplicationSpecifiedCompletionsOn() {
@@ -182,6 +250,10 @@ public final class SettingsValues {
return Arrays.binarySearch(mWordConnectors, code) >= 0;
}
+ public boolean isWordCodePoint(final int code) {
+ return Character.isLetter(code) || isWordConnector(code);
+ }
+
public boolean isUsuallyPrecededBySpace(final int code) {
return Arrays.binarySearch(mSymbolsPrecededBySpace, code) >= 0;
}
@@ -197,14 +269,10 @@ public final class SettingsValues {
public boolean isVoiceKeyEnabled(final EditorInfo editorInfo) {
final boolean shortcutImeEnabled = SubtypeSwitcher.getInstance().isShortcutImeEnabled();
final int inputType = (editorInfo != null) ? editorInfo.inputType : 0;
- return shortcutImeEnabled && mVoiceKeyEnabled
+ return shortcutImeEnabled && mShowsVoiceInputKey
&& !InputTypeUtils.isPasswordInputType(inputType);
}
- public boolean isVoiceKeyOnMain() {
- return mVoiceKeyOnMain;
- }
-
public boolean isLanguageSwitchKeyEnabled() {
if (!mShowsLanguageSwitchKey) {
return false;
@@ -229,7 +297,9 @@ public final class SettingsValues {
// TODO: Stop using KeySpceParser.getLabel().
puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec),
SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED,
- Dictionary.TYPE_HARDCODED));
+ Dictionary.DICTIONARY_HARDCODED,
+ SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+ SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
}
}
return new SuggestedWords(puncList,
@@ -294,4 +364,18 @@ 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();
+ }
+ return prefs.getBoolean(Settings.PREF_VOICE_INPUT_KEY, true);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java
index 604ebeeb6..050d8d26f 100644
--- a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java
+++ b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java
@@ -25,10 +25,10 @@ import android.content.pm.PackageManager;
import android.os.Process;
import android.preference.PreferenceManager;
import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
import com.android.inputmethod.compat.IntentCompatUtils;
-import com.android.inputmethod.latin.RichInputMethodManager;
-import com.android.inputmethod.latin.Settings;
+import com.android.inputmethod.latin.settings.Settings;
/**
* This class detects the {@link Intent#ACTION_MY_PACKAGE_REPLACED} broadcast intent when this IME
@@ -65,17 +65,16 @@ public final class LauncherIconVisibilityManager extends BroadcastReceiver {
}
// The process that hosts this broadcast receiver is invoked and remains alive even after
- // 1) the package has been re-installed, 2) the device has been booted,
- // 3) a multiuser has been created.
+ // 1) the package has been re-installed, 2) the device has just booted,
+ // 3) a new user has been created.
// There is no good reason to keep the process alive if this IME isn't a current IME.
- final boolean isCurrentImeOfCurrentUser;
- if (RichInputMethodManager.isInputMethodManagerValidForUserOfThisProcess(context)) {
- RichInputMethodManager.init(context);
- isCurrentImeOfCurrentUser = SetupActivity.isThisImeCurrent(context);
- } else {
- isCurrentImeOfCurrentUser = false;
- }
-
+ final InputMethodManager imm =
+ (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ // Called to check whether this IME has been triggered by the current user or not
+ final boolean isInputMethodManagerValidForUserOfThisProcess =
+ !imm.getInputMethodList().isEmpty();
+ final boolean isCurrentImeOfCurrentUser = isInputMethodManagerValidForUserOfThisProcess
+ && SetupActivity.isThisImeCurrent(context, imm);
if (!isCurrentImeOfCurrentUser) {
final int myPid = Process.myPid();
Log.i(TAG, "Killing my process: pid=" + myPid);
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
index 8a2de887d..a68f98fe7 100644
--- a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
+++ b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
@@ -24,8 +24,6 @@ import android.provider.Settings;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
-import com.android.inputmethod.latin.RichInputMethodManager;
-
public final class SetupActivity extends Activity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
@@ -40,17 +38,24 @@ public final class SetupActivity extends Activity {
}
}
+ /*
+ * We may not be able to get our own {@link InputMethodInfo} just after this IME is installed
+ * because {@link InputMethodManagerService} may not be aware of this IME yet.
+ * Note: {@link RichInputMethodManager} has similar methods. Here in setup wizard, we can't
+ * use it for the reason above.
+ */
+
/**
* Check if the IME specified by the context is enabled.
- * Note that {@link RichInputMethodManager} must have been initialized before calling this
- * method.
+ * CAVEAT: This may cause a round trip IPC.
*
* @param context package context of the IME to be checked.
+ * @param imm the {@link InputMethodManager}.
* @return true if this IME is enabled.
*/
- public static boolean isThisImeEnabled(final Context context) {
+ /* package */ static boolean isThisImeEnabled(final Context context,
+ final InputMethodManager imm) {
final String packageName = context.getPackageName();
- final InputMethodManager imm = RichInputMethodManager.getInstance().getInputMethodManager();
for (final InputMethodInfo imi : imm.getEnabledInputMethodList()) {
if (packageName.equals(imi.getPackageName())) {
return true;
@@ -61,17 +66,36 @@ public final class SetupActivity extends Activity {
/**
* Check if the IME specified by the context is the current IME.
- * Note that {@link RichInputMethodManager} must have been initialized before calling this
- * method.
+ * CAVEAT: This may cause a round trip IPC.
*
* @param context package context of the IME to be checked.
+ * @param imm the {@link InputMethodManager}.
* @return true if this IME is the current IME.
*/
- public static boolean isThisImeCurrent(final Context context) {
- final InputMethodInfo myImi =
- RichInputMethodManager.getInstance().getInputMethodInfoOfThisIme();
+ /* package */ static boolean isThisImeCurrent(final Context context,
+ final InputMethodManager imm) {
+ final InputMethodInfo imi = getInputMethodInfoOf(context.getPackageName(), imm);
final String currentImeId = Settings.Secure.getString(
context.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);
- return myImi.getId().equals(currentImeId);
+ return imi != null && imi.getId().equals(currentImeId);
+ }
+
+ /**
+ * Get {@link InputMethodInfo} of the IME specified by the package name.
+ * CAVEAT: This may cause a round trip IPC.
+ *
+ * @param packageName package name of the IME.
+ * @param imm the {@link InputMethodManager}.
+ * @return the {@link InputMethodInfo} of the IME specified by the <code>packageName</code>,
+ * or null if not found.
+ */
+ /* package */ static InputMethodInfo getInputMethodInfoOf(final String packageName,
+ final InputMethodManager imm) {
+ for (final InputMethodInfo imi : imm.getInputMethodList()) {
+ if (packageName.equals(imi.getPackageName())) {
+ return imi;
+ }
+ }
+ return null;
}
}
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
index 78a6478c6..c4a813c24 100644
--- a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
+++ b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
@@ -28,17 +28,17 @@ import android.provider.Settings;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.VideoView;
import com.android.inputmethod.compat.TextViewCompatUtils;
import com.android.inputmethod.compat.ViewCompatUtils;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.RichInputMethodManager;
-import com.android.inputmethod.latin.SettingsActivity;
-import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+import com.android.inputmethod.latin.settings.SettingsActivity;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
import java.util.ArrayList;
@@ -48,6 +48,8 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
private static final boolean ENABLE_WELCOME_VIDEO = true;
+ private InputMethodManager mImm;
+
private View mSetupWizard;
private View mWelcomeScreen;
private View mSetupScreen;
@@ -69,15 +71,19 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
private static final int STEP_LAUNCHING_IME_SETTINGS = 4;
private static final int STEP_BACK_FROM_IME_SETTINGS = 5;
- final SettingsPoolingHandler mHandler = new SettingsPoolingHandler(this);
+ private SettingsPoolingHandler mHandler;
- static final class SettingsPoolingHandler
+ private static final class SettingsPoolingHandler
extends StaticInnerHandlerWrapper<SetupWizardActivity> {
private static final int MSG_POLLING_IME_SETTINGS = 0;
private static final long IME_SETTINGS_POLLING_INTERVAL = 200;
- public SettingsPoolingHandler(final SetupWizardActivity outerInstance) {
+ private final InputMethodManager mImmInHandler;
+
+ public SettingsPoolingHandler(final SetupWizardActivity outerInstance,
+ final InputMethodManager imm) {
super(outerInstance);
+ mImmInHandler = imm;
}
@Override
@@ -88,7 +94,7 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
}
switch (msg.what) {
case MSG_POLLING_IME_SETTINGS:
- if (SetupActivity.isThisImeEnabled(setupWizardActivity)) {
+ if (SetupActivity.isThisImeEnabled(setupWizardActivity, mImmInHandler)) {
setupWizardActivity.invokeSetupWizardOfThisIme();
return;
}
@@ -112,11 +118,12 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
setTheme(android.R.style.Theme_Translucent_NoTitleBar);
super.onCreate(savedInstanceState);
+ mImm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
+ mHandler = new SettingsPoolingHandler(this, mImm);
+
setContentView(R.layout.setup_wizard);
mSetupWizard = findViewById(R.id.setup_wizard);
- RichInputMethodManager.init(this);
-
if (savedInstanceState == null) {
mStepNumber = determineSetupStepNumberFromLauncher();
} else {
@@ -143,11 +150,12 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
R.string.setup_step1_title, R.string.setup_step1_instruction,
R.string.setup_step1_finished_instruction, R.drawable.ic_setup_step1,
R.string.setup_step1_action);
+ final SettingsPoolingHandler handler = mHandler;
step1.setAction(new Runnable() {
@Override
public void run() {
invokeLanguageAndInputSettings();
- mHandler.startPollingImeSettings();
+ handler.startPollingImeSettings();
}
});
mSetupStepGroup.addStep(step1);
@@ -265,14 +273,15 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
void invokeInputMethodPicker() {
// Invoke input method picker.
- RichInputMethodManager.getInstance().getInputMethodManager()
- .showInputMethodPicker();
+ mImm.showInputMethodPicker();
mNeedsToAdjustStepNumberToSystemState = true;
}
void invokeSubtypeEnablerOfThisIme() {
- final InputMethodInfo imi =
- RichInputMethodManager.getInstance().getInputMethodInfoOfThisIme();
+ final InputMethodInfo imi = SetupActivity.getInputMethodInfoOf(getPackageName(), mImm);
+ if (imi == null) {
+ return;
+ }
final Intent intent = new Intent();
intent.setAction(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);
intent.addCategory(Intent.CATEGORY_DEFAULT);
@@ -293,10 +302,10 @@ public final class SetupWizardActivity extends Activity implements View.OnClickL
private int determineSetupStepNumber() {
mHandler.cancelPollingImeSettings();
- if (!SetupActivity.isThisImeEnabled(this)) {
+ if (!SetupActivity.isThisImeEnabled(this, mImm)) {
return STEP_1;
}
- if (!SetupActivity.isThisImeCurrent(this)) {
+ if (!SetupActivity.isThisImeCurrent(this, mImm)) {
return STEP_2;
}
return STEP_3;
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 13fcaf48a..eb6d7c106 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -20,22 +20,26 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.service.textservice.SpellCheckerService;
+import android.text.InputType;
import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodSubtype;
import android.view.textservice.SuggestionsInfo;
import com.android.inputmethod.keyboard.KeyboardLayoutSet;
import com.android.inputmethod.latin.BinaryDictionary;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.ContactsBinaryDictionary;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.DictionaryCollection;
import com.android.inputmethod.latin.DictionaryFactory;
-import com.android.inputmethod.latin.LocaleUtils;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.StringUtils;
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.CollectionUtils;
+import com.android.inputmethod.latin.utils.LocaleUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -58,6 +62,9 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
+ private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480;
+ private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 368;
+
private final static String[] EMPTY_STRING_ARRAY = new String[0];
private Map<String, DictionaryPool> mDictionaryPools = CollectionUtils.newSynchronizedTreeMap();
private Map<String, UserBinaryDictionary> mUserDictionaries =
@@ -401,9 +408,9 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
public DictAndKeyboard createDictAndKeyboard(final Locale locale) {
final int script = getScriptFromLocale(locale);
final String keyboardLayoutName = getKeyboardLayoutNameForScript(script);
- final KeyboardLayoutSet keyboardLayoutSet =
- KeyboardLayoutSet.createKeyboardSetForSpellChecker(this, locale.toString(),
- keyboardLayoutName);
+ final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype(
+ locale.toString(), keyboardLayoutName, null);
+ final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype);
final DictionaryCollection dictionaryCollection =
DictionaryFactory.createMainDictionaryFromManager(this, locale,
@@ -431,4 +438,16 @@ public final class AndroidSpellCheckerService extends SpellCheckerService
}
return new DictAndKeyboard(dictionaryCollection, keyboardLayoutSet);
}
+
+ private KeyboardLayoutSet createKeyboardSetForSpellChecker(final InputMethodSubtype subtype) {
+ final EditorInfo editorInfo = new EditorInfo();
+ editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
+ final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(this, editorInfo);
+ builder.setKeyboardGeometry(
+ SPELLCHECKER_DUMMY_KEYBOARD_WIDTH, SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT);
+ builder.setSubtype(subtype);
+ builder.setIsSpellChecker(true /* isSpellChecker */);
+ builder.disableTouchPositionCorrectionData();
+ return builder.build();
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
index 9a1114f7f..ddda52d71 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
@@ -23,7 +23,7 @@ import android.view.textservice.SentenceSuggestionsInfo;
import android.view.textservice.SuggestionsInfo;
import android.view.textservice.TextInfo;
-import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.ArrayList;
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index 16e9fb77e..69f9a467f 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -30,11 +30,11 @@ import android.view.textservice.TextInfo;
import com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.LocaleUtils;
-import com.android.inputmethod.latin.StringUtils;
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.LocaleUtils;
+import com.android.inputmethod.latin.utils.StringUtils;
import java.util.ArrayList;
import java.util.Locale;
@@ -301,12 +301,14 @@ public abstract class AndroidWordLevelSpellCheckerSession extends Session {
// 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 */);
- for (final SuggestedWordInfo suggestion : suggestions) {
- final String suggestionStr = suggestion.mWord;
- suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
- suggestionStr.length(), suggestion.mScore);
+ dictInfo.getProximityInfo(), true /* blockOffensiveWords */,
+ null /* additionalFeaturesOptions */);
+ if (suggestions != null) {
+ for (final SuggestedWordInfo suggestion : suggestions) {
+ final String suggestionStr = suggestion.mWord;
+ suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
+ suggestionStr.length(), suggestion.mScore);
+ }
}
isInDict = isInDictForAnyCapitalization(dictInfo.mDictionary, text, capitalizeType);
} finally {
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
index a20e09ee8..a0aed2829 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
@@ -19,10 +19,10 @@ package com.android.inputmethod.latin.spellcheck;
import android.util.Log;
import com.android.inputmethod.keyboard.ProximityInfo;
-import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import java.util.ArrayList;
import java.util.Locale;
@@ -52,7 +52,7 @@ public final class DictionaryPool extends LinkedBlockingQueue<DictAndKeyboard> {
@Override
public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
final String prevWord, final ProximityInfo proximityInfo,
- final boolean blockOffensiveWords) {
+ final boolean blockOffensiveWords, final int[] additionalFeaturesOptions) {
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 5ce9d8e47..999ca775b 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
@@ -21,7 +21,7 @@ import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.utils.ApplicationUtils;
/**
* Preference screen.
@@ -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(Utils.getAcitivityTitleResId(
+ preferenceScreen.setTitle(ApplicationUtils.getAcitivityTitleResId(
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 322ae5b0f..acd47450b 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -24,14 +24,13 @@ import android.graphics.drawable.Drawable;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardActionListener;
-import com.android.inputmethod.keyboard.TypefaceUtils;
import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
import com.android.inputmethod.keyboard.internal.KeyboardParams;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.utils.TypefaceUtils;
public final class MoreSuggestions extends Keyboard {
public static final int SUGGESTION_CODE_BASE = 1024;
@@ -207,11 +206,12 @@ public final class MoreSuggestions extends Keyboard {
final int y = params.getY(index);
final int width = params.getWidth(index);
final String word = mSuggestedWords.getWord(index);
- final String info = Utils.getDebugInfo(mSuggestedWords, 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, x, y, width, params.mDefaultRowHeight, 0);
+ null /* outputText */, x, y, width, params.mDefaultRowHeight,
+ 0 /* labelFlags */, Key.BACKGROUND_TYPE_NORMAL);
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 d585b5c7f..0ebe37782 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
@@ -34,7 +34,7 @@ public final class MoreSuggestionsView extends MoreKeysKeyboardView {
private static final String TAG = MoreSuggestionsView.class.getSimpleName();
public MoreSuggestionsView(final Context context, final AttributeSet attrs) {
- this(context, attrs, R.attr.moreSuggestionsViewStyle);
+ this(context, attrs, R.attr.moreKeysKeyboardViewStyle);
}
public MoreSuggestionsView(final Context context, final AttributeSet attrs,
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index f434a1211..8d2689a7d 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -45,13 +45,12 @@ import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
-import com.android.inputmethod.keyboard.ViewLayoutUtils;
-import com.android.inputmethod.latin.AutoCorrection;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResourceUtils;
import com.android.inputmethod.latin.SuggestedWords;
-import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
+import com.android.inputmethod.latin.utils.ResourceUtils;
+import com.android.inputmethod.latin.utils.ViewLayoutUtils;
import java.util.ArrayList;
@@ -86,6 +85,7 @@ final class SuggestionStripLayoutHelper {
private final float mAlphaObsoleted;
private final float mCenterSuggestionWeight;
private final int mCenterPositionInStrip;
+ private final int mTypedWordPositionWhenAutocorrect;
private final Drawable mMoreSuggestionsHint;
private static final String MORE_SUGGESTIONS_HINT = "\u2026";
private static final String LEFTWARDS_ARROW = "\u2190";
@@ -122,27 +122,15 @@ final class SuggestionStripLayoutHelper {
mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
final TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle);
+ R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripView);
mSuggestionStripOption = a.getInt(
R.styleable.SuggestionStripView_suggestionStripOption, 0);
- final float alphaValidTypedWord = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f);
- final float alphaTypedWord = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaTypedWord, 1.0f);
- final float alphaAutoCorrect = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f);
- final float alphaSuggested = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
mAlphaObsoleted = ResourceUtils.getFraction(a,
- R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
- mColorValidTypedWord = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord);
- mColorTypedWord = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorTypedWord, 0), alphaTypedWord);
- mColorAutoCorrect = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorAutoCorrect, 0), alphaAutoCorrect);
- mColorSuggested = applyAlpha(a.getColor(
- R.styleable.SuggestionStripView_colorSuggested, 0), alphaSuggested);
+ R.styleable.SuggestionStripView_alphaObsoleted, 1.0f);
+ mColorValidTypedWord = a.getColor(R.styleable.SuggestionStripView_colorValidTypedWord, 0);
+ mColorTypedWord = a.getColor(R.styleable.SuggestionStripView_colorTypedWord, 0);
+ mColorAutoCorrect = a.getColor(R.styleable.SuggestionStripView_colorAutoCorrect, 0);
+ mColorSuggested = a.getColor(R.styleable.SuggestionStripView_colorSuggested, 0);
mSuggestionsCountInStrip = a.getInt(
R.styleable.SuggestionStripView_suggestionsCountInStrip,
DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
@@ -159,6 +147,10 @@ final class SuggestionStripLayoutHelper {
mMoreSuggestionsHint = getMoreSuggestionsHint(res,
res.getDimension(R.dimen.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);
@@ -233,24 +225,31 @@ final class SuggestionStripLayoutHelper {
return spannedWord;
}
- private int getIndexInSuggestedWords(final int positionInStrip,
+ private int getPositionInSuggestionStrip(final int indexInSuggestedWords,
final SuggestedWords suggestedWords) {
- // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more
- // suggestions.
- final int mostImportantIndexInSuggestedWords = suggestedWords.willAutoCorrect()
- ? SuggestedWords.INDEX_OF_AUTO_CORRECTION : SuggestedWords.INDEX_OF_TYPED_WORD;
- if (positionInStrip == mCenterPositionInStrip) {
- return mostImportantIndexInSuggestedWords;
+ final int indexToDisplayMostImportantSuggestion;
+ final int indexToDisplaySecondMostImportantSuggestion;
+ if (suggestedWords.willAutoCorrect()) {
+ indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION;
+ indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD;
+ } else {
+ indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD;
+ indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION;
}
- if (positionInStrip == mostImportantIndexInSuggestedWords) {
+ if (indexInSuggestedWords == indexToDisplayMostImportantSuggestion) {
return mCenterPositionInStrip;
}
- return positionInStrip;
+ if (indexInSuggestedWords == indexToDisplaySecondMostImportantSuggestion) {
+ return mTypedWordPositionWhenAutocorrect;
+ }
+ // If neither of those, the order in the suggestion strip is the same as in SuggestedWords.
+ return indexInSuggestedWords;
}
- private int getSuggestionTextColor(final int positionInStrip,
+ private int getSuggestionTextColor(final int indexInSuggestedWords,
final SuggestedWords suggestedWords) {
- final int indexInSuggestedWords = getIndexInSuggestedWords(positionInStrip, suggestedWords);
+ final int positionInStrip =
+ getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords);
// TODO: Need to revisit this logic with bigram suggestions
final boolean isSuggested = (indexInSuggestedWords != SuggestedWords.INDEX_OF_TYPED_WORD);
@@ -268,7 +267,7 @@ final class SuggestionStripLayoutHelper {
// If we auto-correct, then the autocorrection is in slot 0 and the typed word
// is in slot 1.
if (positionInStrip == mCenterPositionInStrip
- && AutoCorrection.shouldBlockAutoCorrectionBySafetyNet(
+ && AutoCorrectionUtils.shouldBlockAutoCorrectionBySafetyNet(
suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION),
suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD))) {
return 0xFFFF0000;
@@ -352,7 +351,7 @@ final class SuggestionStripLayoutHelper {
* increase towards the right for LTR scripts and the left for RTL scripts, starting with 0.
* The position of the most important suggestion is in {@link #mCenterPositionInStrip}. This
* usually doesn't match the index in <code>suggedtedWords</code> -- see
- * {@link #getIndexInSuggestedWords(int,SuggestedWords)}.
+ * {@link #getPositionInSuggestionStrip(int,SuggestedWords)}.
*
* @param positionInStrip the position in the suggestion strip.
* @param width the maximum width for layout in pixels.
@@ -413,10 +412,19 @@ final class SuggestionStripLayoutHelper {
private void setupWordViewsTextAndColor(final SuggestedWords suggestedWords,
final int countInStrip) {
+ // Clear all suggestions first
+ for (int positionInStrip = 0; positionInStrip < countInStrip; ++positionInStrip) {
+ mWordViews.get(positionInStrip).setText(null);
+ // Make this inactive for touches in {@link #layoutWord(int,int)}.
+ if (SuggestionStripView.DBG) {
+ mDebugInfoViews.get(positionInStrip).setText(null);
+ }
+ }
final int count = Math.min(suggestedWords.size(), countInStrip);
- for (int positionInStrip = 0; positionInStrip < count; positionInStrip++) {
- final int indexInSuggestedWords =
- getIndexInSuggestedWords(positionInStrip, suggestedWords);
+ for (int indexInSuggestedWords = 0; indexInSuggestedWords < count;
+ indexInSuggestedWords++) {
+ final int positionInStrip =
+ getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords);
final TextView wordView = mWordViews.get(positionInStrip);
// {@link TextView#getTag()} is used to get the index in suggestedWords at
// {@link SuggestionStripView#onClick(View)}.
@@ -425,14 +433,7 @@ final class SuggestionStripLayoutHelper {
wordView.setTextColor(getSuggestionTextColor(positionInStrip, suggestedWords));
if (SuggestionStripView.DBG) {
mDebugInfoViews.get(positionInStrip).setText(
- Utils.getDebugInfo(suggestedWords, indexInSuggestedWords));
- }
- }
- for (int positionInStrip = count; positionInStrip < countInStrip; positionInStrip++) {
- mWordViews.get(positionInStrip).setText(null);
- // Make this inactive for touches in {@link #layoutWord(int,int)}.
- if (SuggestionStripView.DBG) {
- mDebugInfoViews.get(positionInStrip).setText(null);
+ suggestedWords.getDebugString(indexInSuggestedWords));
}
}
}
@@ -449,8 +450,10 @@ final class SuggestionStripLayoutHelper {
final TextView wordView = mWordViews.get(positionInStrip);
wordView.setEnabled(true);
wordView.setTextColor(mColorAutoCorrect);
- final String punctuation = suggestedWords.getWord(positionInStrip);
- wordView.setText(punctuation);
+ // {@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.setTextScaleX(1.0f);
wordView.setCompoundDrawables(null, null, null, null);
stripView.addView(wordView);
@@ -468,6 +471,8 @@ final class SuggestionStripLayoutHelper {
final int wordWidth = (int)(width * mCenterSuggestionWeight);
final CharSequence text = 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.setTextScaleX(wordScaleX);
@@ -497,8 +502,10 @@ final class SuggestionStripLayoutHelper {
hintView.setOnClickListener(listener);
}
- public CharSequence getAddToDictionaryWord() {
- return (CharSequence)mWordToSaveView.getTag();
+ 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) {
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index b2b9427af..75f17c559 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -34,7 +34,6 @@ 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.CollectionUtils;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
@@ -42,6 +41,7 @@ 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.suggestions.MoreSuggestions.MoreSuggestionsListener;
+import com.android.inputmethod.latin.utils.CollectionUtils;
import com.android.inputmethod.research.ResearchLogger;
import java.util.ArrayList;
@@ -162,27 +162,27 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
mSuggestionsStrip.removeAllViews();
removeAllViews();
addView(mSuggestionsStrip);
- dismissMoreSuggestions();
+ mMoreSuggestionsView.dismissMoreKeysPanel();
}
private final MoreSuggestionsListener mMoreSuggestionsListener = new MoreSuggestionsListener() {
@Override
public void onSuggestionSelected(final int index, final SuggestedWordInfo wordInfo) {
mListener.pickSuggestionManually(index, wordInfo);
- dismissMoreSuggestions();
+ mMoreSuggestionsView.dismissMoreKeysPanel();
}
@Override
public void onCancelInput() {
- dismissMoreSuggestions();
+ mMoreSuggestionsView.dismissMoreKeysPanel();
}
};
private final MoreKeysPanel.Controller mMoreSuggestionsController =
new MoreKeysPanel.Controller() {
@Override
- public boolean onDismissMoreKeysPanel() {
- return mMainKeyboardView.onDismissMoreKeysPanel();
+ public void onDismissMoreKeysPanel(final MoreKeysPanel panel) {
+ mMainKeyboardView.onDismissMoreKeysPanel(panel);
}
@Override
@@ -191,18 +191,14 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
}
@Override
- public void onCancelMoreKeysPanel() {
- dismissMoreSuggestions();
+ public void onCancelMoreKeysPanel(final MoreKeysPanel panel) {
+ mMoreSuggestionsView.dismissMoreKeysPanel();
}
};
- boolean dismissMoreSuggestions() {
- return mMoreSuggestionsView.dismissMoreKeysPanel();
- }
-
@Override
public boolean onLongClick(final View view) {
- AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(
+ AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(
Constants.NOT_A_CODE, this);
return showMoreSuggestions();
}
@@ -274,15 +270,10 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
return super.dispatchTouchEvent(me);
}
- final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView;
final int action = me.getAction();
- final long eventTime = me.getEventTime();
final int index = me.getActionIndex();
- final int id = me.getPointerId(index);
final int x = (int)me.getX(index);
final int y = (int)me.getY(index);
- final int translatedX = moreKeysPanel.translateX(x);
- final int translatedY = moreKeysPanel.translateY(y);
if (mMoreSuggestionsMode == MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING) {
if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance
@@ -299,19 +290,23 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
}
// MORE_SUGGESTIONS_IN_SLIDING_MODE
- mMoreSuggestionsView.processMotionEvent(action, translatedX, translatedY, id, eventTime);
+ me.setLocation(mMoreSuggestionsView.translateX(x), mMoreSuggestionsView.translateY(y));
+ mMoreSuggestionsView.onTouchEvent(me);
return true;
}
@Override
public void onClick(final View view) {
if (mLayoutHelper.isAddToDictionaryShowing(view)) {
- mListener.addWordToUserDictionary(mLayoutHelper.getAddToDictionaryWord().toString());
+ mListener.addWordToUserDictionary(mLayoutHelper.getAddToDictionaryWord());
clear();
return;
}
final Object tag = view.getTag();
+ // Integer tag is set at
+ // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and
+ // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup}
if (!(tag instanceof Integer)) {
return;
}
@@ -327,6 +322,6 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
- dismissMoreSuggestions();
+ mMoreSuggestionsView.dismissMoreKeysPanel();
}
}
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java
index 2b6fda381..21426d1eb 100644
--- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java
@@ -16,10 +16,6 @@
package com.android.inputmethod.latin.userdictionary;
-import com.android.inputmethod.compat.UserDictionaryCompatUtils;
-import com.android.inputmethod.latin.LocaleUtils;
-import com.android.inputmethod.latin.R;
-
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
@@ -30,6 +26,10 @@ import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
+import com.android.inputmethod.compat.UserDictionaryCompatUtils;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.LocaleUtils;
+
import java.util.ArrayList;
import java.util.Locale;
import java.util.TreeSet;
@@ -65,6 +65,8 @@ public class UserDictionaryAddWordContents {
private String mLocale;
private final String mOldWord;
private final String mOldShortcut;
+ private String mSavedWord;
+ private String mSavedShortcut;
/* package */ UserDictionaryAddWordContents(final View view, final Bundle args) {
mWordEditText = (EditText)view.findViewById(R.id.user_dictionary_add_word_text);
@@ -76,7 +78,9 @@ public class UserDictionaryAddWordContents {
final String word = args.getString(EXTRA_WORD);
if (null != word) {
mWordEditText.setText(word);
- mWordEditText.setSelection(word.length());
+ // Use getText in case the edit text modified the text we set. This happens when
+ // it's too long to be edited.
+ mWordEditText.setSelection(mWordEditText.getText().length());
}
final String shortcut;
if (UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) {
@@ -94,6 +98,16 @@ public class UserDictionaryAddWordContents {
updateLocale(args.getString(EXTRA_LOCALE));
}
+ /* package */ UserDictionaryAddWordContents(final View view,
+ final UserDictionaryAddWordContents oldInstanceToBeEdited) {
+ mWordEditText = (EditText)view.findViewById(R.id.user_dictionary_add_word_text);
+ mShortcutEditText = (EditText)view.findViewById(R.id.user_dictionary_add_shortcut);
+ mMode = MODE_EDIT;
+ mOldWord = oldInstanceToBeEdited.mSavedWord;
+ mOldShortcut = oldInstanceToBeEdited.mSavedShortcut;
+ updateLocale(mLocale);
+ }
+
// locale may be null, this means default locale
// It may also be the empty string, which means "all locales"
/* package */ void updateLocale(final String locale) {
@@ -147,6 +161,8 @@ public class UserDictionaryAddWordContents {
// If the word is somehow empty, don't insert it.
return CODE_CANCEL;
}
+ mSavedWord = newWord;
+ mSavedShortcut = newShortcut;
// If there is no shortcut, and the word already exists in the database, then we
// should not insert, because either A. the word exists with no shortcut, in which
// case the exact same thing we want to insert is already there, or B. the word
@@ -258,4 +274,8 @@ public class UserDictionaryAddWordContents {
localesList.add(new LocaleRenderer(activity, null)); // meaning: select another locale
return localesList;
}
+
+ public String getCurrentUserDictionaryLocale() {
+ return mLocale;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java
index 58c8f266c..4fc132f68 100644
--- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java
@@ -57,23 +57,39 @@ public class UserDictionaryAddWordFragment extends Fragment
private boolean mIsDeleting = false;
@Override
- public void onActivityCreated(Bundle savedInstanceState) {
+ public void onActivityCreated(final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
+ getActivity().getActionBar().setTitle(R.string.edit_personal_dictionary);
+ // Keep the instance so that we remember mContents when configuration changes (eg rotation)
+ setRetainInstance(true);
}
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
+ final Bundle savedState) {
mRootView = inflater.inflate(R.layout.user_dictionary_add_word_fullscreen, null);
mIsDeleting = false;
+ // If we have a non-null mContents object, it's the old value before a configuration
+ // change (eg rotation) so we need to use its values. Otherwise, read from the arguments.
if (null == mContents) {
mContents = new UserDictionaryAddWordContents(mRootView, getArguments());
+ } else {
+ // We create a new mContents object to account for the new situation : a word has
+ // been added to the user dictionary when we started rotating, and we are now editing
+ // it. That means in particular if the word undergoes any change, the old version should
+ // be updated, so the mContents object needs to switch to EDIT mode if it was in
+ // INSERT mode.
+ mContents = new UserDictionaryAddWordContents(mRootView,
+ mContents /* oldInstanceToBeEdited */);
}
+ getActivity().getActionBar().setSubtitle(UserDictionarySettingsUtils.getLocaleDisplayName(
+ getActivity(), mContents.getCurrentUserDictionaryLocale()));
return mRootView;
}
@Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
final MenuItem actionItemAdd = menu.add(0, OPTIONS_MENU_ADD, 0,
R.string.user_dict_settings_add_menu_title).setIcon(R.drawable.ic_menu_add);
actionItemAdd.setShowAsAction(
@@ -87,7 +103,7 @@ public class UserDictionaryAddWordFragment extends Fragment
/**
* Callback for the framework when a menu option is pressed.
*
- * @param MenuItem the item that was pressed
+ * @param item the item that was pressed
* @return false to allow normal menu processing to proceed, true to consume it here
*/
@Override
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
index 6e64882b6..32c4950da 100644
--- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
@@ -16,10 +16,8 @@
package com.android.inputmethod.latin.userdictionary;
-import com.android.inputmethod.latin.LocaleUtils;
-import com.android.inputmethod.latin.R;
-
import android.app.Activity;
+import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
@@ -28,7 +26,14 @@ import android.preference.PreferenceFragment;
import android.preference.PreferenceGroup;
import android.provider.UserDictionary;
import android.text.TextUtils;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.LocaleUtils;
+import java.util.List;
import java.util.Locale;
import java.util.TreeSet;
@@ -52,8 +57,7 @@ public class UserDictionaryList extends PreferenceFragment {
final Cursor cursor = activity.managedQuery(UserDictionary.Words.CONTENT_URI,
new String[] { UserDictionary.Words.LOCALE },
null, null, null);
- final TreeSet<String> localeList = new TreeSet<String>();
- boolean addedAllLocale = false;
+ final TreeSet<String> localeSet = new TreeSet<String>();
if (null == cursor) {
// The user dictionary service is not present or disabled. Return null.
return null;
@@ -61,20 +65,39 @@ public class UserDictionaryList extends PreferenceFragment {
final int columnIndex = cursor.getColumnIndex(UserDictionary.Words.LOCALE);
do {
final String locale = cursor.getString(columnIndex);
- final boolean allLocale = TextUtils.isEmpty(locale);
- localeList.add(allLocale ? "" : locale);
- if (allLocale) {
- addedAllLocale = true;
- }
+ localeSet.add(null != locale ? locale : "");
} while (cursor.moveToNext());
}
- if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED && !addedAllLocale) {
+ if (!UserDictionarySettings.IS_SHORTCUT_API_SUPPORTED) {
// For ICS, we need to show "For all languages" in case that the keyboard locale
// is different from the system locale
- localeList.add("");
+ localeSet.add("");
+ }
+
+ final InputMethodManager imm =
+ (InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+ final List<InputMethodInfo> imis = imm.getEnabledInputMethodList();
+ for (final InputMethodInfo imi : imis) {
+ final List<InputMethodSubtype> subtypes =
+ imm.getEnabledInputMethodSubtypeList(
+ imi, true /* allowsImplicitlySelectedSubtypes */);
+ for (InputMethodSubtype subtype : subtypes) {
+ final String locale = subtype.getLocale();
+ if (!TextUtils.isEmpty(locale)) {
+ localeSet.add(locale);
+ }
+ }
+ }
+
+ // We come here after we have collected locales from existing user dictionary entries and
+ // enabled subtypes. If we already have the locale-without-country version of the system
+ // locale, we don't add the system locale to avoid confusion even though it's technically
+ // correct to add it.
+ if (!localeSet.contains(Locale.getDefault().getLanguage().toString())) {
+ localeSet.add(Locale.getDefault().toString());
}
- localeList.add(Locale.getDefault().toString());
- return localeList;
+
+ return localeSet;
}
/**
@@ -84,13 +107,19 @@ public class UserDictionaryList extends PreferenceFragment {
protected void createUserDictSettings(PreferenceGroup userDictGroup) {
final Activity activity = getActivity();
userDictGroup.removeAll();
- final TreeSet<String> localeList =
+ final TreeSet<String> localeSet =
UserDictionaryList.getUserDictionaryLocalesSet(activity);
- if (localeList.isEmpty()) {
+ if (localeSet.size() > 1) {
+ // Have an "All languages" entry in the languages list if there are two or more active
+ // languages
+ localeSet.add("");
+ }
+
+ if (localeSet.isEmpty()) {
userDictGroup.addPreference(createUserDictionaryPreference(null, activity));
} else {
- for (String locale : localeList) {
+ for (String locale : localeSet) {
userDictGroup.addPreference(createUserDictionaryPreference(locale, activity));
}
}
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
index 50dda9663..7571e87c5 100644
--- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
@@ -1,17 +1,17 @@
-/**
- * Copyright (C) 2013 Google Inc.
+/*
+ * 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
+ * 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
+ * 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.
+ * 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.userdictionary;
@@ -150,7 +150,9 @@ public class UserDictionarySettings extends ListFragment {
listView.setEmptyView(emptyView);
setHasOptionsMenu(true);
-
+ // Show the language as a subtitle of the action bar
+ getActivity().getActionBar().setSubtitle(
+ UserDictionarySettingsUtils.getLocaleDisplayName(getActivity(), mLocale));
}
@SuppressWarnings("deprecation")
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java
new file mode 100644
index 000000000..e58727ec4
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java
@@ -0,0 +1,42 @@
+/*
+ * 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.userdictionary;
+
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.LocaleUtils;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+import java.util.Locale;
+
+/**
+ * Utilities of the user dictionary settings
+ * TODO: We really want to move these utilities to a static library.
+ */
+public class UserDictionarySettingsUtils {
+ public static String getLocaleDisplayName(Context context, String localeStr) {
+ if (TextUtils.isEmpty(localeStr)) {
+ // CAVEAT: localeStr should not be null because a null locale stands for the system
+ // locale in UserDictionary.Words.addWord.
+ return context.getResources().getString(R.string.user_dict_settings_all_languages);
+ }
+ final Locale locale = LocaleUtils.constructLocaleFromString(localeStr);
+ final Locale systemLocale = context.getResources().getConfiguration().locale;
+ return locale.getDisplayName(systemLocale);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
index 85b14d849..44b201642 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
+++ b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+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.IS_ADDITIONAL_SUBTYPE;
@@ -25,12 +25,15 @@ import android.os.Build;
import android.text.TextUtils;
import android.view.inputmethod.InputMethodSubtype;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.R;
+
import java.util.ArrayList;
-public final class AdditionalSubtype {
+public final class AdditionalSubtypeUtils {
private static final InputMethodSubtype[] EMPTY_SUBTYPE_ARRAY = new InputMethodSubtype[0];
- private AdditionalSubtype() {
+ private AdditionalSubtypeUtils() {
// This utility class is not publicly instantiable.
}
@@ -46,8 +49,8 @@ public final class AdditionalSubtype {
final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
final String layoutDisplayNameExtraValue;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
- && SubtypeLocale.isExceptionalLocale(localeString)) {
- final String layoutDisplayName = SubtypeLocale.getKeyboardLayoutSetDisplayName(
+ && SubtypeLocaleUtils.isExceptionalLocale(localeString)) {
+ final String layoutDisplayName = SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(
keyboardLayoutSetName);
layoutDisplayNameExtraValue = StringUtils.appendToCommaSplittableTextIfNotExists(
UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + layoutDisplayName, extraValue);
@@ -57,15 +60,16 @@ public final class AdditionalSubtype {
final String additionalSubtypeExtraValue =
StringUtils.appendToCommaSplittableTextIfNotExists(
IS_ADDITIONAL_SUBTYPE, layoutDisplayNameExtraValue);
- final int nameId = SubtypeLocale.getSubtypeNameId(localeString, keyboardLayoutSetName);
- return new InputMethodSubtype(nameId, R.drawable.ic_subtype_keyboard,
- localeString, KEYBOARD_MODE,
- layoutExtraValue + "," + additionalSubtypeExtraValue, false, false);
+ final int nameId = SubtypeLocaleUtils.getSubtypeNameId(localeString, keyboardLayoutSetName);
+ return new InputMethodSubtype(nameId, R.drawable.ic_ime_switcher_dark,
+ localeString, KEYBOARD_MODE, layoutExtraValue + "," + additionalSubtypeExtraValue
+ + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
+ + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE, false, false);
}
public static String getPrefSubtype(final InputMethodSubtype subtype) {
final String localeString = subtype.getLocale();
- final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
+ final String keyboardLayoutSetName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
final String extraValue = StringUtils.removeFromCommaSplittableTextIfExists(
layoutExtraValue, StringUtils.removeFromCommaSplittableTextIfExists(
@@ -96,7 +100,7 @@ public final class AdditionalSubtype {
CollectionUtils.newArrayList(prefSubtypeArray.length);
for (final String prefSubtype : prefSubtypeArray) {
final InputMethodSubtype subtype = createAdditionalSubtype(prefSubtype);
- if (subtype.getNameResId() == SubtypeLocale.UNKNOWN_KEYBOARD_LAYOUT) {
+ if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) {
// Skip unknown keyboard layout subtype. This may happen when predefined keyboard
// layout has been removed.
continue;
diff --git a/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java
new file mode 100644
index 000000000..08a2a8c5a
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/ApplicationUtils.java
@@ -0,0 +1,65 @@
+/*
+ * 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.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.Log;
+
+public final class ApplicationUtils {
+ private static final String TAG = ApplicationUtils.class.getSimpleName();
+
+ private ApplicationUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static int getAcitivityTitleResId(final Context context,
+ final Class<? extends Activity> cls) {
+ final ComponentName cn = new ComponentName(context, cls);
+ try {
+ final ActivityInfo ai = context.getPackageManager().getActivityInfo(cn, 0);
+ if (ai != null) {
+ return ai.labelRes;
+ }
+ } catch (final NameNotFoundException e) {
+ Log.e(TAG, "Failed to get settings activity title res id.", e);
+ }
+ return 0;
+ }
+
+ /**
+ * A utility method to get the application's PackageInfo.versionName
+ * @return the application's PackageInfo.versionName
+ */
+ public static String getVersionName(final Context context) {
+ try {
+ if (context == null) {
+ return "";
+ }
+ final String packageName = context.getPackageName();
+ final PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
+ return info.versionName;
+ } catch (final NameNotFoundException e) {
+ Log.e(TAG, "Could not find version info.", e);
+ }
+ return "";
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
new file mode 100644
index 000000000..c2e97a36f
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/AsyncResultHolder.java
@@ -0,0 +1,71 @@
+/*
+ * 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.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class is a holder of a result of asynchronous computation.
+ *
+ * @param <E> the type of the result.
+ */
+public class AsyncResultHolder<E> {
+
+ private final Object mLock = new Object();
+
+ private E mResult;
+ private final CountDownLatch mLatch;
+
+ public AsyncResultHolder() {
+ mLatch = new CountDownLatch(1);
+ }
+
+ /**
+ * Sets the result value to this holder.
+ *
+ * @param result the value which is set.
+ */
+ public void set(final E result) {
+ synchronized(mLock) {
+ if (mLatch.getCount() > 0) {
+ mResult = result;
+ mLatch.countDown();
+ }
+ }
+ }
+
+ /**
+ * Gets the result value held in this holder.
+ * 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.
+ */
+ public E get(final E defaultValue, final long timeOut) {
+ try {
+ if(mLatch.await(timeOut, TimeUnit.MILLISECONDS)) {
+ return mResult;
+ } else {
+ return defaultValue;
+ }
+ } catch (InterruptedException e) {
+ return defaultValue;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
index 86be4295a..066c5fd32 100644
--- a/java/src/com/android/inputmethod/latin/AutoCorrection.java
+++ b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
@@ -14,8 +14,12 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+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;
@@ -23,12 +27,12 @@ import android.util.Log;
import java.util.concurrent.ConcurrentHashMap;
-public final class AutoCorrection {
+public final class AutoCorrectionUtils {
private static final boolean DBG = LatinImeLogger.sDBG;
- private static final String TAG = AutoCorrection.class.getSimpleName();
+ private static final String TAG = AutoCorrectionUtils.class.getSimpleName();
private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
- private AutoCorrection() {
+ private AutoCorrectionUtils() {
// Purely static class: can't instantiate.
}
diff --git a/java/src/com/android/inputmethod/latin/BoundedTreeSet.java b/java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java
index 489a74ef1..ae1fd3f79 100644
--- a/java/src/com/android/inputmethod/latin/BoundedTreeSet.java
+++ b/java/src/com/android/inputmethod/latin/utils/BoundedTreeSet.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
diff --git a/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java b/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java
new file mode 100644
index 000000000..2028298f2
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/ByteArrayDictBuffer.java
@@ -0,0 +1,81 @@
+/*
+ * 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/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
index 4b8d1ac11..60b24d5d5 100644
--- a/java/src/com/android/inputmethod/latin/CapsModeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
@@ -14,11 +14,14 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.text.InputType;
import android.text.TextUtils;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.WordComposer;
+
import java.util.Locale;
public final class CapsModeUtils {
@@ -57,6 +60,11 @@ public final class CapsModeUtils {
|| WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == mode;
}
+ private static boolean isPeriod(final int codePoint) {
+ // TODO: make this a resource.
+ return codePoint == Constants.CODE_PERIOD || codePoint == Constants.CODE_ARMENIAN_PERIOD;
+ }
+
/**
* Determine what caps mode should be in effect at the current offset in
* the text. Only the mode bits set in <var>reqModes</var> will be
@@ -187,7 +195,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 (c != Constants.CODE_PERIOD || j <= 0) {
+ if (!isPeriod(c) || j <= 0) {
return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
}
@@ -237,7 +245,7 @@ public final class CapsModeUtils {
case WORD:
if (Character.isLetter(c)) {
state = WORD;
- } else if (c == Constants.CODE_PERIOD) {
+ } else if (isPeriod(c)) {
state = PERIOD;
} else {
return caps;
@@ -253,7 +261,7 @@ public final class CapsModeUtils {
case LETTER:
if (Character.isLetter(c)) {
state = LETTER;
- } else if (c == Constants.CODE_PERIOD) {
+ } else if (isPeriod(c)) {
state = PERIOD;
} else {
return noCaps;
diff --git a/java/src/com/android/inputmethod/latin/CollectionUtils.java b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
index a8623cc63..cc25102ce 100644
--- a/java/src/com/android/inputmethod/latin/CollectionUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.util.SparseArray;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -94,6 +95,10 @@ public final class CollectionUtils {
return new CopyOnWriteArrayList<E>(array);
}
+ public static <E> ArrayDeque<E> newArrayDeque() {
+ return new ArrayDeque<E>();
+ }
+
public static <E> SparseArray<E> newSparseArray() {
return new SparseArray<E>();
}
diff --git a/java/src/com/android/inputmethod/latin/CompletionInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/CompletionInfoUtils.java
index 792a446c9..5ccf0e079 100644
--- a/java/src/com/android/inputmethod/latin/CompletionInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CompletionInfoUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.text.TextUtils;
import android.view.inputmethod.CompletionInfo;
diff --git a/java/src/com/android/inputmethod/latin/CoordinateUtils.java b/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java
index af270e1e4..72f2cd2d9 100644
--- a/java/src/com/android/inputmethod/latin/CoordinateUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
public final class CoordinateUtils {
private static final int INDEX_X = 0;
diff --git a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
index 999c2f0de..36b927eea 100644
--- a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
@@ -17,13 +17,12 @@
package com.android.inputmethod.latin.utils;
import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.latin.CollectionUtils;
import java.util.ArrayList;
/**
* Utility methods for parsing and serializing Comma-Separated Values. The public APIs of this
- * utility class are {@link #split(String)}, {@link #split(int,String)}, {@link #join(String)},
+ * utility class are {@link #split(String)}, {@link #split(int,String)}, {@link #join(String...)},
* {@link #join(int,String...)}, and {@link #join(int,int[],String...)}.
*
* This class implements CSV parsing and serializing methods conforming to RFC 4180 with an
diff --git a/java/src/com/android/inputmethod/dictionarypack/Utils.java b/java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java
index c4a42dbbf..ac654fa65 100644
--- a/java/src/com/android/inputmethod/dictionarypack/Utils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DebugLogUtils.java
@@ -14,16 +14,18 @@
* the License.
*/
-package com.android.inputmethod.dictionarypack;
+package com.android.inputmethod.latin.utils;
import android.util.Log;
+import com.android.inputmethod.latin.LatinImeLogger;
+
/**
- * A class for various utility methods, especially debugging.
+ * A class for logging and debugging utility methods.
*/
-public final class Utils {
- private final static String TAG = Utils.class.getSimpleName() + ":DEBUG --";
- private final static boolean DEBUG = DictionaryProvider.DEBUG;
+public final class DebugLogUtils {
+ private final static String TAG = DebugLogUtils.class.getSimpleName();
+ private final static boolean sDBG = LatinImeLogger.sDBG;
/**
* Calls .toString() on its non-null argument or returns "null"
@@ -39,13 +41,22 @@ public final class Utils {
* @return a readable, carriage-return-separated string for the current stack trace.
*/
public static String getStackTrace() {
+ return getStackTrace(Integer.MAX_VALUE - 1);
+ }
+
+ /**
+ * Get the string representation of the current stack trace, for debugging purposes.
+ * @param limit the maximum number of stack frames to be returned.
+ * @return a readable, carriage-return-separated string for the current stack trace.
+ */
+ public static String getStackTrace(final int limit) {
final StringBuilder sb = new StringBuilder();
try {
throw new RuntimeException();
- } catch (RuntimeException e) {
- StackTraceElement[] frames = e.getStackTrace();
+ } catch (final RuntimeException e) {
+ final StackTraceElement[] frames = e.getStackTrace();
// Start at 1 because the first frame is here and we don't care about it
- for (int j = 1; j < frames.length; ++j) {
+ for (int j = 1; j < frames.length && j < limit + 1; ++j) {
sb.append(frames[j].toString() + "\n");
}
}
@@ -54,12 +65,12 @@ public final class Utils {
/**
* Get the stack trace contained in an exception as a human-readable string.
- * @param e the exception
+ * @param t the throwable
* @return the human-readable stack trace
*/
- public static String getStackTrace(final Exception e) {
+ public static String getStackTrace(final Throwable t) {
final StringBuilder sb = new StringBuilder();
- final StackTraceElement[] frames = e.getStackTrace();
+ final StackTraceElement[] frames = t.getStackTrace();
for (int j = 0; j < frames.length; ++j) {
sb.append(frames[j].toString() + "\n");
}
@@ -75,7 +86,7 @@ public final class Utils {
* @param args the stuff to send to the log
*/
public static void l(final Object... args) {
- if (!DEBUG) return;
+ if (!sDBG) return;
final StringBuilder sb = new StringBuilder();
for (final Object o : args) {
sb.append(s(o).toString());
@@ -92,7 +103,7 @@ public final class Utils {
* @param args the stuff to send to the log
*/
public static void r(final Object... args) {
- if (!DEBUG) return;
+ if (!sDBG) return;
final StringBuilder sb = new StringBuilder("\u001B[31m");
for (final Object o : args) {
sb.append(s(o).toString());
diff --git a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
index 9d478491a..021bf0825 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
@@ -14,33 +14,32 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
-import android.text.format.DateUtils;
import android.util.Log;
+import com.android.inputmethod.latin.AssetFileAddress;
+import com.android.inputmethod.latin.BinaryDictionaryGetter;
+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.UnsupportedFormatException;
import java.io.File;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Locale;
+import java.util.concurrent.TimeUnit;
/**
* This class encapsulates the logic for the Latin-IME side of dictionary information management.
*/
public class DictionaryInfoUtils {
private static final String TAG = DictionaryInfoUtils.class.getSimpleName();
- // This class must be located in the same package as LatinIME.java.
- private static final String RESOURCE_PACKAGE_NAME =
- DictionaryInfoUtils.class.getPackage().getName();
+ private static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName();
private static final String DEFAULT_MAIN_DICT = "main";
private static final String MAIN_DICT_PREFIX = "main_";
// 6 digits - unicode is limited to 21 bits
@@ -73,8 +72,8 @@ public class DictionaryInfoUtils {
values.put(LOCALE_COLUMN, mLocale.toString());
values.put(DESCRIPTION_COLUMN, mDescription);
values.put(LOCAL_FILENAME_COLUMN, mFileAddress.mFilename);
- values.put(DATE_COLUMN,
- new File(mFileAddress.mFilename).lastModified() / DateUtils.SECOND_IN_MILLIS);
+ values.put(DATE_COLUMN, TimeUnit.MILLISECONDS.toSeconds(
+ new File(mFileAddress.mFilename).lastModified()));
values.put(FILESIZE_COLUMN, mFileAddress.mLength);
values.put(VERSION_COLUMN, mVersion);
return values;
@@ -280,13 +279,7 @@ public class DictionaryInfoUtils {
}
public static FileHeader getDictionaryFileHeaderOrNull(final File file) {
- try {
- return BinaryDictIOUtils.getDictionaryFileHeader(file, 0, file.length());
- } catch (UnsupportedFormatException e) {
- return null;
- } catch (IOException e) {
- return null;
- }
+ return BinaryDictIOUtils.getDictionaryFileHeaderOrNull(file, 0, file.length());
}
private static DictionaryInfo createDictionaryInfoFromFileAddress(
diff --git a/java/src/com/android/inputmethod/latin/FeedbackUtils.java b/java/src/com/android/inputmethod/latin/utils/FeedbackUtils.java
index 0582763fe..ec7eaf4a0 100644
--- a/java/src/com/android/inputmethod/latin/FeedbackUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/FeedbackUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.content.Context;
import android.content.Intent;
diff --git a/java/src/com/android/inputmethod/latin/FileTransforms.java b/java/src/com/android/inputmethod/latin/utils/FileTransforms.java
index 692f3c7c1..9f4584ec9 100644
--- a/java/src/com/android/inputmethod/latin/FileTransforms.java
+++ b/java/src/com/android/inputmethod/latin/utils/FileTransforms.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import java.io.IOException;
import java.io.InputStream;
diff --git a/java/src/com/android/inputmethod/latin/InputTypeUtils.java b/java/src/com/android/inputmethod/latin/utils/InputTypeUtils.java
index 46194f6e4..19cd34011 100644
--- a/java/src/com/android/inputmethod/latin/InputTypeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/InputTypeUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.text.InputType;
import android.view.inputmethod.EditorInfo;
diff --git a/java/src/com/android/inputmethod/latin/IntentUtils.java b/java/src/com/android/inputmethod/latin/utils/IntentUtils.java
index d175af504..ea0168117 100644
--- a/java/src/com/android/inputmethod/latin/IntentUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/IntentUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.content.Intent;
import android.text.TextUtils;
diff --git a/java/src/com/android/inputmethod/latin/JniUtils.java b/java/src/com/android/inputmethod/latin/utils/JniUtils.java
index 8aedee576..e7fdafaeb 100644
--- a/java/src/com/android/inputmethod/latin/JniUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/JniUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.util.Log;
diff --git a/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java b/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.java
new file mode 100644
index 000000000..e958a7e71
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/LatinImeLoggerUtils.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.latin.utils;
+
+import android.text.TextUtils;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.WordComposer;
+
+public final class LatinImeLoggerUtils {
+ private LatinImeLoggerUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static void onNonSeparator(final char code, final int x, final int y) {
+ UserLogRingCharBuffer.getInstance().push(code, x, y);
+ LatinImeLogger.logOnInputChar();
+ }
+
+ 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);
+ }
+
+ public static void onSeparator(final String separator, final int x, final int y) {
+ final int length = separator.length();
+ for (int i = 0; i < length; i = Character.offsetByCodePoints(separator, i, 1)) {
+ int codePoint = Character.codePointAt(separator, i);
+ // TODO: accept code points
+ UserLogRingCharBuffer.getInstance().push((char)codePoint, x, y);
+ }
+ LatinImeLogger.logOnInputSeparator();
+ }
+
+ public static void onAutoCorrection(final String typedWord, final String correctedWord,
+ final String separatorString, final WordComposer wordComposer) {
+ final boolean isBatchMode = wordComposer.isBatchMode();
+ if (!isBatchMode && TextUtils.isEmpty(typedWord)) {
+ return;
+ }
+ // TODO: this fails when the separator is more than 1 code point long, but
+ // the backend can't handle it yet. The only case when this happens is with
+ // smileys and other multi-character keys.
+ final int codePoint = TextUtils.isEmpty(separatorString) ? Constants.NOT_A_CODE
+ : separatorString.codePointAt(0);
+ if (!isBatchMode) {
+ LatinImeLogger.logOnAutoCorrectionForTyping(typedWord, correctedWord, codePoint);
+ } else {
+ if (!TextUtils.isEmpty(correctedWord)) {
+ // We must make sure that InputPointer contains only the relative timestamps,
+ // not actual timestamps.
+ LatinImeLogger.logOnAutoCorrectionForGeometric(
+ "", correctedWord, codePoint, wordComposer.getInputPointers());
+ }
+ }
+ }
+
+ public static void onAutoCorrectionCancellation() {
+ LatinImeLogger.logOnAutoCorrectionCancelled();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/LocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java
index a1e40502e..22045aa38 100644
--- a/java/src/com/android/inputmethod/latin/LocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java
@@ -14,10 +14,8 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
-import android.content.res.Configuration;
-import android.content.res.Resources;
import android.text.TextUtils;
import java.util.HashMap;
@@ -164,39 +162,6 @@ public final class LocaleUtils {
return LOCALE_MATCH <= level;
}
- static final Object sLockForRunInLocale = new Object();
-
- public abstract static class RunInLocale<T> {
- protected abstract T job(Resources res);
-
- /**
- * Execute {@link #job(Resources)} method in specified system locale exclusively.
- *
- * @param res the resources to use. Pass current resources.
- * @param newLocale the locale to change to
- * @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));
- try {
- if (needsChange) {
- conf.locale = newLocale;
- res.updateConfiguration(conf, null);
- }
- return job(res);
- } finally {
- if (needsChange) {
- conf.locale = oldLocale;
- res.updateConfiguration(conf, null);
- }
- }
- }
- }
- }
-
private static final HashMap<String, Locale> sLocaleCache = CollectionUtils.newHashMap();
/**
diff --git a/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java b/java/src/com/android/inputmethod/latin/utils/MetadataFileUriGetter.java
index a98ecc7b6..9ad319da6 100644
--- a/java/src/com/android/inputmethod/latin/MetadataFileUriGetter.java
+++ b/java/src/com/android/inputmethod/latin/utils/MetadataFileUriGetter.java
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
+
+import com.android.inputmethod.latin.R;
import android.content.Context;
diff --git a/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
new file mode 100644
index 000000000..5dc0b5893
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/PrioritizedSerialExecutor.java
@@ -0,0 +1,147 @@
+/*
+ * 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.util.ArrayDeque;
+import java.util.Queue;
+
+/**
+ * An object that executes submitted tasks using a thread.
+ */
+public class PrioritizedSerialExecutor {
+ public static final String TAG = PrioritizedSerialExecutor.class.getSimpleName();
+
+ private final Object mLock = new Object();
+
+ // The default value of capacities of task queues.
+ private static final int TASK_QUEUE_CAPACITY = 1000;
+ private final Queue<Runnable> mTasks;
+ private final Queue<Runnable> mPrioritizedTasks;
+ private boolean mIsShutdown;
+
+ // The task which is running now.
+ private Runnable mActive;
+
+ public PrioritizedSerialExecutor() {
+ mTasks = new ArrayDeque<Runnable>(TASK_QUEUE_CAPACITY);
+ mPrioritizedTasks = new ArrayDeque<Runnable>(TASK_QUEUE_CAPACITY);
+ mIsShutdown = false;
+ }
+
+ /**
+ * Clears all queued tasks.
+ */
+ public void clearAllTasks() {
+ synchronized(mLock) {
+ mTasks.clear();
+ mPrioritizedTasks.clear();
+ }
+ }
+
+ /**
+ * Enqueues the given task into the task queue.
+ * @param r the enqueued task
+ */
+ public void execute(final Runnable r) {
+ synchronized(mLock) {
+ if (!mIsShutdown) {
+ mTasks.offer(r);
+ if (mActive == null) {
+ scheduleNext();
+ }
+ }
+ }
+ }
+
+ /**
+ * Enqueues the given task into the prioritized task queue.
+ * @param r the enqueued task
+ */
+ public void executePrioritized(final Runnable r) {
+ synchronized(mLock) {
+ if (!mIsShutdown) {
+ mPrioritizedTasks.offer(r);
+ if (mActive == null) {
+ scheduleNext();
+ }
+ }
+ }
+ }
+
+ private boolean fetchNextTasks() {
+ synchronized(mLock) {
+ mActive = mPrioritizedTasks.poll();
+ if (mActive == null) {
+ mActive = mTasks.poll();
+ }
+ return mActive != null;
+ }
+ }
+
+ private void scheduleNext() {
+ synchronized(mLock) {
+ if (!fetchNextTasks()) {
+ return;
+ }
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ do {
+ synchronized(mLock) {
+ if (mActive != null) {
+ mActive.run();
+ }
+ }
+ } while (fetchNextTasks());
+ } finally {
+ scheduleNext();
+ }
+ }
+ }).start();
+ }
+ }
+
+ public void remove(final Runnable r) {
+ synchronized(mLock) {
+ mTasks.remove(r);
+ mPrioritizedTasks.remove(r);
+ }
+ }
+
+ public void replaceAndExecute(final Runnable oldTask, final Runnable newTask) {
+ synchronized(mLock) {
+ if (oldTask != null) remove(oldTask);
+ execute(newTask);
+ }
+ }
+
+ public void shutdown() {
+ synchronized(mLock) {
+ mIsShutdown = true;
+ }
+ }
+
+ public boolean isTerminated() {
+ synchronized(mLock) {
+ if (!mIsShutdown) {
+ return false;
+ }
+ return mPrioritizedTasks.isEmpty() && mTasks.isEmpty() && mActive == null;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
index 8a704ab42..0f5cd80db 100644
--- a/java/src/com/android/inputmethod/latin/RecapitalizeStatus.java
+++ b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
@@ -14,9 +14,7 @@
* the License.
*/
-package com.android.inputmethod.latin;
-
-import com.android.inputmethod.latin.StringUtils;
+package com.android.inputmethod.latin.utils;
import java.util.Locale;
@@ -163,7 +161,10 @@ public class RecapitalizeStatus {
final int codePoint = mStringBefore.codePointBefore(nonWhitespaceEnd);
if (!Character.isWhitespace(codePoint)) break;
}
- if (0 != nonWhitespaceStart || len != nonWhitespaceEnd) {
+ // If nonWhitespaceStart >= nonWhitespaceEnd, that means the selection contained only
+ // whitespace, so we leave it as is.
+ if ((0 != nonWhitespaceStart || len != nonWhitespaceEnd)
+ && nonWhitespaceStart < nonWhitespaceEnd) {
mCursorEndAfter = mCursorStartBefore + nonWhitespaceEnd;
mCursorStartBefore = mCursorStartAfter = mCursorStartBefore + nonWhitespaceStart;
mStringAfter = mStringBefore =
diff --git a/java/src/com/android/inputmethod/latin/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
index 691f0602a..7c6fe93ac 100644
--- a/java/src/com/android/inputmethod/latin/ResizableIntArray.java
+++ b/java/src/com/android/inputmethod/latin/utils/ResizableIntArray.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import java.util.Arrays;
@@ -132,6 +132,15 @@ public final class ResizableIntArray {
}
}
+ /**
+ * Shift to the left by elementCount, discarding elementCount pointers at the start.
+ * @param elementCount how many elements to shift.
+ */
+ public void shift(final int elementCount) {
+ System.arraycopy(mArray, elementCount, mArray, 0, mLength - elementCount);
+ mLength -= elementCount;
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
diff --git a/java/src/com/android/inputmethod/latin/ResourceUtils.java b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
index 0eb8b4f09..22c92446a 100644
--- a/java/src/com/android/inputmethod/latin/ResourceUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ResourceUtils.java
@@ -14,16 +14,18 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Build;
import android.text.TextUtils;
+import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.R;
import java.util.ArrayList;
import java.util.HashMap;
@@ -215,6 +217,35 @@ public final class ResourceUtils {
return null;
}
+ public static int getDefaultKeyboardWidth(final Resources res) {
+ final DisplayMetrics dm = res.getDisplayMetrics();
+ return dm.widthPixels;
+ }
+
+ public static int getDefaultKeyboardHeight(final Resources res) {
+ final DisplayMetrics dm = res.getDisplayMetrics();
+ final String keyboardHeightString = getDeviceOverrideValue(res, R.array.keyboard_heights);
+ final float keyboardHeight;
+ if (TextUtils.isEmpty(keyboardHeightString)) {
+ keyboardHeight = res.getDimension(R.dimen.keyboardHeight);
+ } else {
+ keyboardHeight = Float.parseFloat(keyboardHeightString) * dm.density;
+ }
+ final float maxKeyboardHeight = res.getFraction(
+ R.fraction.maxKeyboardHeight, dm.heightPixels, dm.heightPixels);
+ float minKeyboardHeight = res.getFraction(
+ R.fraction.minKeyboardHeight, 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);
+ }
+ // Keyboard height will not exceed maxKeyboardHeight and will not be less than
+ // minKeyboardHeight.
+ return (int)Math.max(Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
+ }
+
public static boolean isValidFraction(final float fraction) {
return fraction >= 0.0f;
}
diff --git a/java/src/com/android/inputmethod/latin/utils/RunInLocale.java b/java/src/com/android/inputmethod/latin/utils/RunInLocale.java
new file mode 100644
index 000000000..2c9e3b191
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/RunInLocale.java
@@ -0,0 +1,55 @@
+/*
+ * 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.content.res.Configuration;
+import android.content.res.Resources;
+
+import java.util.Locale;
+
+public abstract class RunInLocale<T> {
+ private static final Object sLockForRunInLocale = new Object();
+
+ protected abstract T job(final Resources res);
+
+ /**
+ * Execute {@link #job(Resources)} method in specified system locale exclusively.
+ *
+ * @param res the resources to use.
+ * @param newLocale the locale to change to.
+ * @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));
+ try {
+ if (needsChange) {
+ conf.locale = newLocale;
+ res.updateConfiguration(conf, null);
+ }
+ return job(res);
+ } finally {
+ if (needsChange) {
+ conf.locale = oldLocale;
+ res.updateConfiguration(conf, null);
+ }
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java
new file mode 100644
index 000000000..b51fd9377
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/SpannableStringUtils.java
@@ -0,0 +1,110 @@
+/*
+ * 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.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.text.style.SuggestionSpan;
+
+public final class SpannableStringUtils {
+ /**
+ * Copies the spans from the region <code>start...end</code> in
+ * <code>source</code> to the region
+ * <code>destoff...destoff+end-start</code> in <code>dest</code>.
+ * Spans in <code>source</code> that begin before <code>start</code>
+ * or end after <code>end</code> but overlap this range are trimmed
+ * as if they began at <code>start</code> or ended at <code>end</code>.
+ * Only SuggestionSpans that don't have the SPAN_PARAGRAPH span are copied.
+ *
+ * This code is almost entirely taken from {@link TextUtils#copySpansFrom}, except for the
+ * kind of span that is copied.
+ *
+ * @throws IndexOutOfBoundsException if any of the copied spans
+ * are out of range in <code>dest</code>.
+ */
+ public static void copyNonParagraphSuggestionSpansFrom(Spanned source, int start, int end,
+ 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;
+
+ int st = source.getSpanStart(spans[i]);
+ int en = source.getSpanEnd(spans[i]);
+
+ if (st < start)
+ st = start;
+ if (en > end)
+ en = end;
+
+ dest.setSpan(spans[i], st - start + destoff, en - start + destoff,
+ fl);
+ }
+ }
+
+ /**
+ * Returns a CharSequence concatenating the specified CharSequences, retaining their
+ * SuggestionSpans that don't have the PARAGRAPH flag, but not other spans.
+ *
+ * This code is almost entirely taken from {@link TextUtils#concat(CharSequence...)}, except
+ * it calls copyNonParagraphSuggestionSpansFrom instead of {@link TextUtils#copySpansFrom}.
+ */
+ public static CharSequence concatWithNonParagraphSuggestionSpansOnly(CharSequence... text) {
+ if (text.length == 0) {
+ return "";
+ }
+
+ if (text.length == 1) {
+ return text[0];
+ }
+
+ boolean spanned = false;
+ for (int i = 0; i < text.length; i++) {
+ if (text[i] instanceof Spanned) {
+ spanned = true;
+ break;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < text.length; i++) {
+ sb.append(text[i]);
+ }
+
+ if (!spanned) {
+ return sb.toString();
+ }
+
+ SpannableString ss = new SpannableString(sb);
+ int off = 0;
+ for (int i = 0; i < text.length; i++) {
+ int len = text[i].length();
+
+ if (text[i] instanceof Spanned) {
+ copyNonParagraphSuggestionSpansFrom((Spanned) text[i], 0, len, ss, off);
+ }
+
+ off += len;
+ }
+
+ return new SpannedString(ss);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/StaticInnerHandlerWrapper.java b/java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java
index e50af4d2d..44e5d17b4 100644
--- a/java/src/com/android/inputmethod/latin/StaticInnerHandlerWrapper.java
+++ b/java/src/com/android/inputmethod/latin/utils/StaticInnerHandlerWrapper.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.os.Handler;
import android.os.Looper;
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index c2fd4fb32..121aecf0f 100644
--- a/java/src/com/android/inputmethod/latin/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -14,14 +14,27 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+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 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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.Locale;
public final class StringUtils {
+ private static final String TAG = StringUtils.class.getSimpleName();
public static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
public static final int CAPITALIZE_FIRST = 1; // First only
public static final int CAPITALIZE_ALL = 2; // All caps
@@ -191,27 +204,56 @@ public final class StringUtils {
}
public static boolean isIdenticalAfterUpcase(final String text) {
- final int len = text.length();
- for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+ final int length = text.length();
+ int i = 0;
+ while (i < length) {
final int codePoint = text.codePointAt(i);
if (Character.isLetter(codePoint) && !Character.isUpperCase(codePoint)) {
return false;
}
+ i += Character.charCount(codePoint);
}
return true;
}
public static boolean isIdenticalAfterDowncase(final String text) {
- final int len = text.length();
- for (int i = 0; i < len; i = text.offsetByCodePoints(i, 1)) {
+ final int length = text.length();
+ int i = 0;
+ while (i < length) {
final int codePoint = text.codePointAt(i);
if (Character.isLetter(codePoint) && !Character.isLowerCase(codePoint)) {
return false;
}
+ i += Character.charCount(codePoint);
}
return true;
}
+ @UsedForTesting
+ public static boolean looksValidForDictionaryInsertion(final CharSequence text,
+ final SettingsValues settings) {
+ if (TextUtils.isEmpty(text)) return false;
+ 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;
+ }
+ if (!settings.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;
+ }
+
public static boolean isIdenticalAfterCapitalizeEachWord(final String text,
final String separators) {
boolean needCapsNext = true;
@@ -314,4 +356,110 @@ public final class StringUtils {
// Otherwise, it doesn't look like an URL.
return false;
}
+
+ public static boolean isEmptyStringOrWhiteSpaces(String s) {
+ final int N = codePointCount(s);
+ for (int i = 0; i < N; ++i) {
+ if (!Character.isWhitespace(s.codePointAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @UsedForTesting
+ public static String byteArrayToHexString(byte[] bytes) {
+ if (bytes == null || bytes.length == 0) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(String.format("%02x", b & 0xff));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Convert hex string to byte array. The string length must be an even number.
+ */
+ @UsedForTesting
+ public static byte[] hexStringToByteArray(String hexString) {
+ if (TextUtils.isEmpty(hexString)) {
+ return null;
+ }
+ final int N = hexString.length();
+ if (N % 2 != 0) {
+ throw new NumberFormatException("Input hex string length must be an even number."
+ + " Length = " + N);
+ }
+ final byte[] bytes = new byte[N / 2];
+ for (int i = 0; i < N; i += 2) {
+ bytes[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
+ + Character.digit(hexString.charAt(i + 1), 16));
+ }
+ 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) {
+ }
+ }
+ 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();
+ }
+ writer.endArray();
+ return sw.toString();
+ } catch (IOException e) {
+ } finally {
+ try {
+ if (writer != null) {
+ writer.close();
+ }
+ } catch (IOException e) {
+ }
+ }
+ return "";
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
index 4d88ecc0c..102a41b4e 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java
+++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
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;
@@ -25,13 +25,14 @@ import android.os.Build;
import android.util.Log;
import android.view.inputmethod.InputMethodSubtype;
-import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
+import com.android.inputmethod.latin.DictionaryFactory;
+import com.android.inputmethod.latin.R;
import java.util.HashMap;
import java.util.Locale;
-public final class SubtypeLocale {
- static final String TAG = SubtypeLocale.class.getSimpleName();
+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();
@@ -39,6 +40,7 @@ public final class SubtypeLocale {
// Special language code to represent "no language".
public static final String NO_LANGUAGE = "zz";
public static final String QWERTY = "qwerty";
+ public static final String EMOJI = "emoji";
public static final int UNKNOWN_KEYBOARD_LAYOUT = R.string.subtype_generic;
private static boolean sInitialized = false;
@@ -69,7 +71,7 @@ public final class SubtypeLocale {
private static final HashMap<String, String> sLocaleAndExtraValueToKeyboardLayoutSetMap =
CollectionUtils.newHashMap();
- private SubtypeLocale() {
+ private SubtypeLocaleUtils() {
// Intentional empty constructor for utility class.
}
@@ -217,9 +219,11 @@ public final class SubtypeLocale {
return getSubtypeDisplayNameInternal(subtype, displayLocale);
}
- public static String getSubtypeDisplayName(final InputMethodSubtype subtype) {
- final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(subtype.getLocale());
- return getSubtypeDisplayNameInternal(subtype, displayLocale);
+ public static String getSubtypeNameForLogging(final InputMethodSubtype subtype) {
+ if (subtype == null) {
+ return "<null subtype>";
+ }
+ return getSubtypeLocale(subtype) + "/" + getKeyboardLayoutSetName(subtype);
}
private static String getSubtypeDisplayNameInternal(final InputMethodSubtype subtype,
@@ -238,7 +242,7 @@ public final class SubtypeLocale {
+ " nameResId=" + subtype.getNameResId()
+ " locale=" + subtype.getLocale()
+ " extra=" + subtype.getExtraValue()
- + "\n" + Utils.getStackTrace());
+ + "\n" + DebugLogUtils.getStackTrace());
return "";
}
}
@@ -284,4 +288,46 @@ public final class SubtypeLocale {
}
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());
+ }
+
+ // 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());
+ }
+
+ // 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);
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/TargetPackageInfoGetterTask.java b/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java
index 947b0c586..afbe2ecad 100644
--- a/java/src/com/android/inputmethod/latin/TargetPackageInfoGetterTask.java
+++ b/java/src/com/android/inputmethod/latin/utils/TargetPackageInfoGetterTask.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.content.Context;
import android.content.pm.PackageInfo;
diff --git a/java/src/com/android/inputmethod/latin/utils/TextRange.java b/java/src/com/android/inputmethod/latin/utils/TextRange.java
new file mode 100644
index 000000000..48b443ddd
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/TextRange.java
@@ -0,0 +1,120 @@
+/*
+ * 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.text.Spanned;
+import android.text.style.SuggestionSpan;
+
+import java.util.Arrays;
+
+/**
+ * Represents a range of text, relative to the current cursor position.
+ */
+public final class TextRange {
+ private final CharSequence mTextAtCursor;
+ private final int mWordAtCursorStartIndex;
+ private final int mWordAtCursorEndIndex;
+ private final int mCursorIndex;
+
+ public final CharSequence mWord;
+
+ public int getNumberOfCharsInWordBeforeCursor() {
+ return mCursorIndex - mWordAtCursorStartIndex;
+ }
+
+ public int getNumberOfCharsInWordAfterCursor() {
+ return mWordAtCursorEndIndex - mCursorIndex;
+ }
+
+ public int length() {
+ return mWord.length();
+ }
+
+ /**
+ * Gets the suggestion spans that are put squarely on the word, with the exact start
+ * and end of the span matching the boundaries of the word.
+ * @return the list of spans.
+ */
+ public SuggestionSpan[] getSuggestionSpansAtWord() {
+ if (!(mTextAtCursor instanceof Spanned && mWord instanceof Spanned)) {
+ return new SuggestionSpan[0];
+ }
+ final Spanned text = (Spanned)mTextAtCursor;
+ // Note: it's fine to pass indices negative or greater than the length of the string
+ // to the #getSpans() method. The reason we need to get from -1 to +1 is that, the
+ // spans were cut at the cursor position, and #getSpans(start, end) does not return
+ // spans that end at `start' or begin at `end'. Consider the following case:
+ // this| is (The | symbolizes the cursor position
+ // ---- ---
+ // In this case, the cursor is in position 4, so the 0~7 span has been split into
+ // a 0~4 part and a 4~7 part.
+ // If we called #getSpans(0, 4) in this case, we would only get the part from 0 to 4
+ // of the span, and not the part from 4 to 7, so we would not realize the span actually
+ // extends from 0 to 7. But if we call #getSpans(-1, 5) we'll get both the 0~4 and
+ // the 4~7 spans and we can merge them accordingly.
+ // Any span starting more than 1 char away from the word boundaries in any direction
+ // does not touch the word, so we don't need to consider it. That's why requesting
+ // -1 ~ +1 is enough.
+ // Of course this is only relevant if the cursor is at one end of the word. If it's
+ // in the middle, the -1 and +1 are not necessary, but they are harmless.
+ final SuggestionSpan[] spans = text.getSpans(mWordAtCursorStartIndex - 1,
+ mWordAtCursorEndIndex + 1, SuggestionSpan.class);
+ int readIndex = 0;
+ int writeIndex = 0;
+ for (; readIndex < spans.length; ++readIndex) {
+ final SuggestionSpan span = spans[readIndex];
+ // The span may be null, as we null them when we find duplicates. Cf a few lines
+ // down.
+ if (null == span) continue;
+ // Tentative span start and end. This may be modified later if we realize the
+ // same span is also applied to other parts of the string.
+ int spanStart = text.getSpanStart(span);
+ int spanEnd = text.getSpanEnd(span);
+ for (int i = readIndex + 1; i < spans.length; ++i) {
+ if (span.equals(spans[i])) {
+ // We found the same span somewhere else. Read the new extent of this
+ // span, and adjust our values accordingly.
+ spanStart = Math.min(spanStart, text.getSpanStart(spans[i]));
+ spanEnd = Math.max(spanEnd, text.getSpanEnd(spans[i]));
+ // ...and mark the span as processed.
+ spans[i] = null;
+ }
+ }
+ if (spanStart == mWordAtCursorStartIndex && spanEnd == mWordAtCursorEndIndex) {
+ // If the span does not start and stop here, we 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];
+ }
+ }
+ return writeIndex == readIndex ? spans : Arrays.copyOfRange(spans, 0, writeIndex);
+ }
+
+ public TextRange(final CharSequence textAtCursor, final int wordAtCursorStartIndex,
+ final int wordAtCursorEndIndex, final int cursorIndex) {
+ if (wordAtCursorStartIndex < 0 || cursorIndex < wordAtCursorStartIndex
+ || cursorIndex > wordAtCursorEndIndex
+ || wordAtCursorEndIndex > textAtCursor.length()) {
+ throw new IndexOutOfBoundsException();
+ }
+ mTextAtCursor = textAtCursor;
+ mWordAtCursorStartIndex = wordAtCursorStartIndex;
+ mWordAtCursorEndIndex = wordAtCursorEndIndex;
+ mCursorIndex = cursorIndex;
+ mWord = mTextAtCursor.subSequence(mWordAtCursorStartIndex, mWordAtCursorEndIndex);
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/inputmethod/keyboard/TypefaceUtils.java b/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java
index 6a54e119c..47ea1ea75 100644
--- a/java/src/com/android/inputmethod/keyboard/TypefaceUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/TypefaceUtils.java
@@ -14,15 +14,13 @@
* limitations under the License.
*/
-package com.android.inputmethod.keyboard;
+package com.android.inputmethod.latin.utils;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.util.SparseArray;
-import com.android.inputmethod.latin.CollectionUtils;
-
public final class TypefaceUtils {
private TypefaceUtils() {
// This utility class is not publicly instantiable.
@@ -68,6 +66,11 @@ 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();
diff --git a/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java b/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java
new file mode 100644
index 000000000..06826dac0
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/UsabilityStudyLogUtils.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2016 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.Intent;
+import android.content.pm.PackageManager;
+import android.inputmethodservice.InputMethodService;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.inputmethod.latin.LatinImeLogger;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.channels.FileChannel;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public final class UsabilityStudyLogUtils {
+ // TODO: remove code duplication with ResearchLog class
+ private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
+ private static final String FILENAME = "log.txt";
+ private final Handler mLoggingHandler;
+ private File mFile;
+ private File mDirectory;
+ private InputMethodService mIms;
+ private PrintWriter mWriter;
+ private final Date mDate;
+ private final SimpleDateFormat mDateFormat;
+
+ private UsabilityStudyLogUtils() {
+ mDate = new Date();
+ mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ", Locale.US);
+
+ HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ handlerThread.start();
+ mLoggingHandler = new Handler(handlerThread.getLooper());
+ }
+
+ // Initialization-on-demand holder
+ private static final class OnDemandInitializationHolder {
+ public static final UsabilityStudyLogUtils sInstance = new UsabilityStudyLogUtils();
+ }
+
+ public static UsabilityStudyLogUtils getInstance() {
+ return OnDemandInitializationHolder.sInstance;
+ }
+
+ public void init(final InputMethodService ims) {
+ mIms = ims;
+ mDirectory = ims.getFilesDir();
+ }
+
+ private void createLogFileIfNotExist() {
+ if ((mFile == null || !mFile.exists())
+ && (mDirectory != null && mDirectory.exists())) {
+ try {
+ mWriter = getPrintWriter(mDirectory, FILENAME, false);
+ } catch (final IOException e) {
+ Log.e(USABILITY_TAG, "Can't create log file.");
+ }
+ }
+ }
+
+ public static void writeBackSpace(final int x, final int y) {
+ UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y);
+ }
+
+ public static void writeChar(final char c, final int x, final int y) {
+ String inputChar = String.valueOf(c);
+ switch (c) {
+ case '\n':
+ inputChar = "<enter>";
+ break;
+ case '\t':
+ inputChar = "<tab>";
+ break;
+ case ' ':
+ inputChar = "<space>";
+ break;
+ }
+ UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y);
+ LatinImeLogger.onPrintAllUsabilityStudyLogs();
+ }
+
+ public static void writeMotionEvent(final MotionEvent me) {
+ final int action = me.getActionMasked();
+ final long eventTime = me.getEventTime();
+ final int pointerCount = me.getPointerCount();
+ for (int index = 0; index < pointerCount; index++) {
+ final int id = me.getPointerId(index);
+ final int x = (int)me.getX(index);
+ final int y = (int)me.getY(index);
+ final float size = me.getSize(index);
+ final float pressure = me.getPressure(index);
+
+ final String eventTag;
+ switch (action) {
+ case MotionEvent.ACTION_UP:
+ eventTag = "[Up]";
+ break;
+ case MotionEvent.ACTION_DOWN:
+ eventTag = "[Down]";
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ eventTag = "[PointerUp]";
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ eventTag = "[PointerDown]";
+ break;
+ case MotionEvent.ACTION_MOVE:
+ eventTag = "[Move]";
+ break;
+ default:
+ eventTag = "[Action" + action + "]";
+ break;
+ }
+ getInstance().write(eventTag + eventTime + "," + id + "," + x + "," + y + "," + size
+ + "," + pressure);
+ }
+ }
+
+ public void write(final String log) {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ createLogFileIfNotExist();
+ final long currentTime = System.currentTimeMillis();
+ mDate.setTime(currentTime);
+
+ final String printString = String.format(Locale.US, "%s\t%d\t%s\n",
+ mDateFormat.format(mDate), currentTime, log);
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Write: " + log);
+ }
+ mWriter.print(printString);
+ }
+ });
+ }
+
+ private synchronized String getBufferedLogs() {
+ mWriter.flush();
+ final StringBuilder sb = new StringBuilder();
+ final BufferedReader br = getBufferedReader();
+ String line;
+ try {
+ while ((line = br.readLine()) != null) {
+ sb.append('\n');
+ sb.append(line);
+ }
+ } catch (final IOException e) {
+ Log.e(USABILITY_TAG, "Can't read log file.");
+ } finally {
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Got all buffered logs\n" + sb.toString());
+ }
+ try {
+ br.close();
+ } catch (final IOException e) {
+ // ignore.
+ }
+ }
+ return sb.toString();
+ }
+
+ public void emailResearcherLogsAll() {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ final Date date = new Date();
+ date.setTime(System.currentTimeMillis());
+ final String currentDateTimeString =
+ new SimpleDateFormat("yyyyMMdd-HHmmssZ", Locale.US).format(date);
+ if (mFile == null) {
+ Log.w(USABILITY_TAG, "No internal log file found.");
+ return;
+ }
+ if (mIms.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.w(USABILITY_TAG, "Doesn't have the permission WRITE_EXTERNAL_STORAGE");
+ return;
+ }
+ mWriter.flush();
+ final String destPath = Environment.getExternalStorageDirectory()
+ + "/research-" + currentDateTimeString + ".log";
+ final File destFile = new File(destPath);
+ try {
+ final FileInputStream srcStream = new FileInputStream(mFile);
+ final FileOutputStream destStream = new FileOutputStream(destFile);
+ final FileChannel src = srcStream.getChannel();
+ final FileChannel dest = destStream.getChannel();
+ src.transferTo(0, src.size(), dest);
+ src.close();
+ srcStream.close();
+ dest.close();
+ destStream.close();
+ } catch (final FileNotFoundException e1) {
+ Log.w(USABILITY_TAG, e1);
+ return;
+ } catch (final IOException e2) {
+ Log.w(USABILITY_TAG, e2);
+ return;
+ }
+ if (!destFile.exists()) {
+ Log.w(USABILITY_TAG, "Dest file doesn't exist.");
+ return;
+ }
+ final Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Destination file URI is " + destFile.toURI());
+ }
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + destPath));
+ intent.putExtra(Intent.EXTRA_SUBJECT,
+ "[Research Logs] " + currentDateTimeString);
+ mIms.startActivity(intent);
+ }
+ });
+ }
+
+ public void printAll() {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mIms.getCurrentInputConnection().commitText(getBufferedLogs(), 0);
+ }
+ });
+ }
+
+ public void clearAll() {
+ mLoggingHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mFile != null && mFile.exists()) {
+ if (LatinImeLogger.sDBG) {
+ Log.d(USABILITY_TAG, "Delete log file.");
+ }
+ mFile.delete();
+ mWriter.close();
+ }
+ }
+ });
+ }
+
+ private BufferedReader getBufferedReader() {
+ createLogFileIfNotExist();
+ try {
+ return new BufferedReader(new FileReader(mFile));
+ } catch (final FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ private PrintWriter getPrintWriter(final File dir, final String filename,
+ final boolean renew) throws IOException {
+ mFile = new File(dir, filename);
+ if (mFile.exists()) {
+ if (renew) {
+ mFile.delete();
+ }
+ }
+ return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
index 10931555e..ea32a74ff 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryDictIOUtils.java
@@ -14,25 +14,27 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+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.BinaryDictInputOutput;
-import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
+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.Node;
+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.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+import java.util.concurrent.TimeUnit;
/**
* Reads and writes Binary files for a UserHistoryDictionary.
@@ -42,6 +44,9 @@ import java.util.Map;
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 {
public void setUnigram(final String word, final String shortcutTarget, final int frequency);
@@ -53,73 +58,18 @@ public final class UserHistoryDictIOUtils {
public int getFrequency(final String word1, final String word2);
}
- public static final class ByteArrayWrapper implements FusionDictionaryBufferInterface {
- private byte[] mBuffer;
- private int mPosition;
-
- public ByteArrayWrapper(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;
- }
- }
-
/**
* Writes dictionary to file.
*/
- public static void writeDictionaryBinary(final OutputStream destination,
+ 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 {
- BinaryDictInputOutput.writeDictionaryBinary(destination, fusionDict, formatOptions);
+ dictEncoder.writeDictionary(fusionDict, formatOptions);
Log.d(TAG, "end writing");
} catch (IOException e) {
Log.e(TAG, "IO exception while writing file", e);
@@ -134,7 +84,7 @@ public final class UserHistoryDictIOUtils {
@UsedForTesting
static FusionDictionary constructFusionDictionary(
final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams) {
- final FusionDictionary fusionDict = new FusionDictionary(new Node(),
+ final FusionDictionary fusionDict = new FusionDictionary(new PtNodeArray(),
new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false,
false));
int profTotal = 0;
@@ -158,7 +108,7 @@ public final class UserHistoryDictIOUtils {
if (word1 == null) { // unigram
fusionDict.add(word2, freq, null, false /* isNotAWord */);
} else { // bigram
- if (FusionDictionary.findWordInTree(fusionDict.mRoot, word1) == null) {
+ if (FusionDictionary.findWordInTree(fusionDict.mRootNodeArray, word1) == null) {
fusionDict.add(word1, 2, null, false /* isNotAWord */);
}
fusionDict.setBigram(word1, word2, freq);
@@ -175,14 +125,13 @@ public final class UserHistoryDictIOUtils {
/**
* Reads dictionary from file.
*/
- public static void readDictionaryBinary(final FusionDictionaryBufferInterface buffer,
+ public static void readDictionaryBinary(final DictDecoder dictDecoder,
final OnAddWordListener dict) {
- final Map<Integer, String> unigrams = CollectionUtils.newTreeMap();
- final Map<Integer, Integer> frequencies = CollectionUtils.newTreeMap();
- final Map<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap();
+ final TreeMap<Integer, String> unigrams = CollectionUtils.newTreeMap();
+ final TreeMap<Integer, Integer> frequencies = CollectionUtils.newTreeMap();
+ final TreeMap<Integer, ArrayList<PendingAttribute>> bigrams = CollectionUtils.newTreeMap();
try {
- BinaryDictIOUtils.readUnigramsAndBigramsBinary(buffer, unigrams, frequencies,
- bigrams);
+ dictDecoder.readUnigramsAndBigramsBinary(unigrams, frequencies, bigrams);
} catch (IOException e) {
Log.e(TAG, "IO exception while reading file", e);
} catch (UnsupportedFormatException e) {
@@ -197,10 +146,11 @@ public final class UserHistoryDictIOUtils {
* Adds all unigrams and bigrams in maps to OnAddWordListener.
*/
@UsedForTesting
- static void addWordsFromWordMap(final Map<Integer, String> unigrams,
- final Map<Integer, Integer> frequencies,
- final Map<Integer, ArrayList<PendingAttribute>> bigrams, final OnAddWordListener to) {
- for (Map.Entry<Integer, String> entry : unigrams.entrySet()) {
+ 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, unigramFrequency);
@@ -213,7 +163,7 @@ public final class UserHistoryDictIOUtils {
continue;
}
to.setBigram(word1, word2,
- BinaryDictInputOutput.reconstructBigramFrequency(unigramFrequency,
+ BinaryDictIOUtils.reconstructBigramFrequency(unigramFrequency,
attr.mFrequency));
}
}
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java
index 9053d709b..1992b2f5d 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/UserHistoryForgettingCurveUtils.java
@@ -14,24 +14,35 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
-import android.text.format.DateUtils;
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 FC_FREQ_MAX = 127;
+ 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 = ELAPSED_TIME_INTERVAL_HOURS
- * DateUtils.HOUR_IN_MILLIS;
+ 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.
}
diff --git a/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java b/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
new file mode 100644
index 000000000..a75d353c9
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/UserLogRingCharBuffer.java
@@ -0,0 +1,137 @@
+/*
+ * 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.inputmethodservice.InputMethodService;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.settings.Settings;
+
+public final class UserLogRingCharBuffer {
+ public /* for test */ static final int BUFSIZE = 20;
+ public /* for test */ int mLength = 0;
+
+ private static UserLogRingCharBuffer sUserLogRingCharBuffer = new UserLogRingCharBuffer();
+ private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
+ private static final int INVALID_COORDINATE = -2;
+ private boolean mEnabled = false;
+ private int mEnd = 0;
+ private char[] mCharBuf = new char[BUFSIZE];
+ private int[] mXBuf = new int[BUFSIZE];
+ private int[] mYBuf = new int[BUFSIZE];
+
+ private UserLogRingCharBuffer() {
+ // Intentional empty constructor for singleton.
+ }
+
+ @UsedForTesting
+ public static UserLogRingCharBuffer getInstance() {
+ return sUserLogRingCharBuffer;
+ }
+
+ public static UserLogRingCharBuffer init(final InputMethodService context,
+ final boolean enabled, final boolean usabilityStudy) {
+ if (!(enabled || usabilityStudy)) {
+ return null;
+ }
+ sUserLogRingCharBuffer.mEnabled = true;
+ UsabilityStudyLogUtils.getInstance().init(context);
+ return sUserLogRingCharBuffer;
+ }
+
+ private static int normalize(final int in) {
+ int ret = in % BUFSIZE;
+ return ret < 0 ? ret + BUFSIZE : ret;
+ }
+
+ // TODO: accept code points
+ @UsedForTesting
+ public void push(final char c, final int x, final int y) {
+ if (!mEnabled) {
+ return;
+ }
+ if (LatinImeLogger.sUsabilityStudy) {
+ UsabilityStudyLogUtils.getInstance().writeChar(c, x, y);
+ }
+ mCharBuf[mEnd] = c;
+ mXBuf[mEnd] = x;
+ mYBuf[mEnd] = y;
+ mEnd = normalize(mEnd + 1);
+ if (mLength < BUFSIZE) {
+ ++mLength;
+ }
+ }
+
+ public char pop() {
+ if (mLength < 1) {
+ return PLACEHOLDER_DELIMITER_CHAR;
+ }
+ mEnd = normalize(mEnd - 1);
+ --mLength;
+ return mCharBuf[mEnd];
+ }
+
+ public char getBackwardNthChar(final int n) {
+ if (mLength <= n || n < 0) {
+ return PLACEHOLDER_DELIMITER_CHAR;
+ }
+ return mCharBuf[normalize(mEnd - n - 1)];
+ }
+
+ public int getPreviousX(final char c, final int back) {
+ final int index = normalize(mEnd - 2 - back);
+ if (mLength <= back
+ || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
+ return INVALID_COORDINATE;
+ }
+ return mXBuf[index];
+ }
+
+ public int getPreviousY(final char c, final int back) {
+ int index = normalize(mEnd - 2 - back);
+ if (mLength <= back
+ || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
+ return INVALID_COORDINATE;
+ }
+ return mYBuf[index];
+ }
+
+ public String getLastWord(final int ignoreCharCount) {
+ final StringBuilder sb = new StringBuilder();
+ int i = ignoreCharCount;
+ for (; i < mLength; ++i) {
+ final char c = mCharBuf[normalize(mEnd - 1 - i)];
+ if (!Settings.getInstance().isWordSeparator(c)) {
+ break;
+ }
+ }
+ for (; i < mLength; ++i) {
+ char c = mCharBuf[normalize(mEnd - 1 - i)];
+ if (!Settings.getInstance().isWordSeparator(c)) {
+ sb.append(c);
+ } else {
+ break;
+ }
+ }
+ return sb.reverse().toString();
+ }
+
+ public void reset() {
+ mLength = 0;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java b/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java
index dc12fa468..f9d853493 100644
--- a/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ViewLayoutUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.keyboard;
+package com.android.inputmethod.latin.utils;
import android.view.View;
import android.view.ViewGroup;
@@ -27,7 +27,8 @@ public final class ViewLayoutUtils {
// This utility class is not publicly instantiable.
}
- public static MarginLayoutParams newLayoutParam(ViewGroup placer, int width, int height) {
+ public static MarginLayoutParams newLayoutParam(final ViewGroup placer, final int width,
+ final int height) {
if (placer instanceof FrameLayout) {
return new FrameLayout.LayoutParams(width, height);
} else if (placer instanceof RelativeLayout) {
@@ -40,7 +41,8 @@ public final class ViewLayoutUtils {
}
}
- public static void placeViewAt(View view, int x, int y, int w, int h) {
+ public static void placeViewAt(final View view, final int x, final int y, final int w,
+ final int h) {
final ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp instanceof MarginLayoutParams) {
final MarginLayoutParams marginLayoutParams = (MarginLayoutParams)lp;
diff --git a/java/src/com/android/inputmethod/latin/XmlParseUtils.java b/java/src/com/android/inputmethod/latin/utils/XmlParseUtils.java
index 48e5ed30a..bdad16652 100644
--- a/java/src/com/android/inputmethod/latin/XmlParseUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/XmlParseUtils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
import android.content.res.TypedArray;
diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java
index b985fda21..520b88d2f 100644
--- a/java/src/com/android/inputmethod/research/FeedbackActivity.java
+++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java
@@ -18,7 +18,6 @@ package com.android.inputmethod.research;
import android.app.Activity;
import android.os.Bundle;
-import android.widget.CheckBox;
import com.android.inputmethod.latin.R;
diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java
index a0738292e..75fbbf1ba 100644
--- a/java/src/com/android/inputmethod/research/FeedbackFragment.java
+++ b/java/src/com/android/inputmethod/research/FeedbackFragment.java
@@ -16,7 +16,6 @@
package com.android.inputmethod.research;
-import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.text.Editable;
diff --git a/java/src/com/android/inputmethod/research/JsonUtils.java b/java/src/com/android/inputmethod/research/JsonUtils.java
index 24cd8d935..2beebdfae 100644
--- a/java/src/com/android/inputmethod/research/JsonUtils.java
+++ b/java/src/com/android/inputmethod/research/JsonUtils.java
@@ -75,12 +75,12 @@ import java.util.Map;
private static void writeJson(final Key key, final JsonWriter jsonWriter) throws IOException {
jsonWriter.beginObject();
- jsonWriter.name("code").value(key.mCode);
+ jsonWriter.name("code").value(key.getCode());
jsonWriter.name("altCode").value(key.getAltCode());
- jsonWriter.name("x").value(key.mX);
- jsonWriter.name("y").value(key.mY);
- jsonWriter.name("w").value(key.mWidth);
- jsonWriter.name("h").value(key.mHeight);
+ jsonWriter.name("x").value(key.getX());
+ jsonWriter.name("y").value(key.getY());
+ jsonWriter.name("w").value(key.getWidth());
+ jsonWriter.name("h").value(key.getHeight());
jsonWriter.endObject();
}
@@ -94,12 +94,17 @@ import java.util.Map;
.value(words.mIsPunctuationSuggestions);
jsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions);
jsonWriter.name("isPrediction").value(words.mIsPrediction);
- jsonWriter.name("words");
+ jsonWriter.name("suggestedWords");
jsonWriter.beginArray();
final int size = words.size();
for (int j = 0; j < size; j++) {
final SuggestedWordInfo wordInfo = words.getInfo(j);
- jsonWriter.value(wordInfo.toString());
+ jsonWriter.beginObject();
+ jsonWriter.name("word").value(wordInfo.toString());
+ jsonWriter.name("score").value(wordInfo.mScore);
+ jsonWriter.name("kind").value(wordInfo.mKind);
+ jsonWriter.name("sourceDict").value(wordInfo.mSourceDict.mDictType);
+ jsonWriter.endObject();
}
jsonWriter.endArray();
jsonWriter.endObject();
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index 164c7e8cc..3366df12a 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -146,7 +146,8 @@ public class LogUnit {
if (size != 0) {
// Note that jsonWriter is only set to a non-null value if the logUnit start text is
// output and at least one logStatement is output.
- JsonWriter jsonWriter = null;
+ JsonWriter jsonWriter = researchLog.getInitializedJsonWriterLocked();
+ outputLogUnitStart(jsonWriter, canIncludePrivateData);
for (int i = 0; i < size; i++) {
final LogStatement logStatement = mLogStatementList.get(i);
if (!canIncludePrivateData && logStatement.isPotentiallyPrivate()) {
@@ -155,42 +156,35 @@ public class LogUnit {
if (mIsPartOfMegaword && logStatement.isPotentiallyRevealing()) {
continue;
}
- // Only retrieve the jsonWriter if we need to. If we don't get this far, then
- // researchLog.getInitializedJsonWriterLocked() will not ever be called, and the
- // file will not have been opened for writing.
- if (jsonWriter == null) {
- jsonWriter = researchLog.getInitializedJsonWriterLocked();
- outputLogUnitStart(jsonWriter, canIncludePrivateData);
- }
logStatement.outputToLocked(jsonWriter, mTimeList.get(i), mValuesList.get(i));
}
- if (jsonWriter != null) {
- // We must have called logUnitStart earlier, so emit a logUnitStop.
- outputLogUnitStop(jsonWriter);
- }
+ outputLogUnitStop(jsonWriter);
}
}
private static final String WORD_KEY = "_wo";
+ private static final String NUM_WORDS_KEY = "_nw";
private static final String CORRECTION_TYPE_KEY = "_corType";
private static final String LOG_UNIT_BEGIN_KEY = "logUnitStart";
private static final String LOG_UNIT_END_KEY = "logUnitEnd";
final LogStatement LOGSTATEMENT_LOG_UNIT_BEGIN_WITH_PRIVATE_DATA =
new LogStatement(LOG_UNIT_BEGIN_KEY, false /* isPotentiallyPrivate */,
- false /* isPotentiallyRevealing */, WORD_KEY, CORRECTION_TYPE_KEY);
+ false /* isPotentiallyRevealing */, WORD_KEY, CORRECTION_TYPE_KEY,
+ NUM_WORDS_KEY);
final LogStatement LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA =
new LogStatement(LOG_UNIT_BEGIN_KEY, false /* isPotentiallyPrivate */,
- false /* isPotentiallyRevealing */);
+ false /* isPotentiallyRevealing */, NUM_WORDS_KEY);
private void outputLogUnitStart(final JsonWriter jsonWriter,
final boolean canIncludePrivateData) {
final LogStatement logStatement;
if (canIncludePrivateData) {
LOGSTATEMENT_LOG_UNIT_BEGIN_WITH_PRIVATE_DATA.outputToLocked(jsonWriter,
- SystemClock.uptimeMillis(), getWordsAsString(), getCorrectionType());
+ SystemClock.uptimeMillis(), getWordsAsString(), getCorrectionType(),
+ getNumWords());
} else {
LOGSTATEMENT_LOG_UNIT_BEGIN_WITHOUT_PRIVATE_DATA.outputToLocked(jsonWriter,
- SystemClock.uptimeMillis());
+ SystemClock.uptimeMillis(), getNumWords());
}
}
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
index 3482153b4..6df7c1708 100644
--- a/java/src/com/android/inputmethod/research/MainLogBuffer.java
+++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java
@@ -119,9 +119,9 @@ public abstract class MainLogBuffer extends FixedLogBuffer {
*
* @param logUnits a LogUnit list to check for publishability
* @param nGramSize the smallest n-gram acceptable to be published. if
- * {@link ResearchLogger.IS_LOGGING_EVERYTHING} is true, then publish if there are more than
+ * {@link ResearchLogger#IS_LOGGING_EVERYTHING} is true, then publish if there are more than
* {@code minNGramSize} words in the logUnits, otherwise wait. if {@link
- * ResearchLogger.IS_LOGGING_EVERYTHING} is false, then ensure that there are exactly nGramSize
+ * ResearchLogger#IS_LOGGING_EVERYTHING} is false, then ensure that there are exactly nGramSize
* words in the LogUnits.
*
* @return one of the {@code PUBLISHABILITY_*} result codes defined in this class.
diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java
index fbfd9b531..3388645b7 100644
--- a/java/src/com/android/inputmethod/research/MotionEventReader.java
+++ b/java/src/com/android/inputmethod/research/MotionEventReader.java
@@ -315,16 +315,6 @@ public class MotionEventReader {
return pointerCoords;
}
- /**
- * Tests that {@code x} is uninitialized.
- *
- * Assumes that {@code x} will never be given a valid value less than 0, and that
- * UNINITIALIZED_FLOAT is less than 0.0f.
- */
- private boolean isUninitializedFloat(final float x) {
- return x < 0.0f;
- }
-
private void addMotionEventData(final ReplayData replayData, final int actionType,
final long time, final PointerProperties[] pointerProperties,
final PointerCoords[] pointerCoords) {
diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java
index fde2798e1..46e620ae5 100644
--- a/java/src/com/android/inputmethod/research/ResearchLog.java
+++ b/java/src/com/android/inputmethod/research/ResearchLog.java
@@ -27,7 +27,6 @@ import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
-import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
@@ -56,7 +55,7 @@ public class ResearchLog {
private static final String TAG = ResearchLog.class.getSimpleName();
private static final boolean DEBUG = false
&& ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
- private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
+ private static final long FLUSH_DELAY_IN_MS = TimeUnit.SECONDS.toMillis(5);
/* package */ final ScheduledExecutorService mExecutor;
/* package */ final File mFile;
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index aa4a866b8..da9c61103 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -20,11 +20,7 @@ import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOAR
import android.accounts.Account;
import android.accounts.AccountManager;
-import android.app.AlertDialog;
-import android.app.Dialog;
import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
@@ -34,7 +30,6 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
-import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -42,12 +37,9 @@ import android.os.IBinder;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.text.TextUtils;
-import android.text.format.DateUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
-import android.view.Window;
-import android.view.WindowManager;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@@ -61,15 +53,16 @@ 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.InputTypeUtils;
import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.RichInputConnection;
-import com.android.inputmethod.latin.RichInputConnection.Range;
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.TextRange;
import com.android.inputmethod.research.MotionEventReader.ReplayData;
+import com.android.inputmethod.research.ui.SplashScreen;
import java.io.File;
import java.io.FileInputStream;
@@ -81,6 +74,7 @@ import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
+import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
// TODO: Add a unit test for every "logging" method (i.e. that is called from the IME and calls
@@ -94,12 +88,12 @@ import java.util.regex.Pattern;
* This functionality is off by default. See
* {@link ProductionFlag#USES_DEVELOPMENT_ONLY_DIAGNOSTICS}.
*/
-public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
+public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener,
+ SplashScreen.UserConsentListener {
// TODO: This class has grown quite large and combines several concerns that should be
// separated. The following refactorings will be applied as soon as possible after adding
// support for replaying historical events, fixing some replay bugs, adding some ui constraints
// on the feedback dialog, and adding the survey dialog.
- // TODO: Refactor. Move splash screen code into separate class.
// TODO: Refactor. Move feedback screen code into separate class.
// TODO: Refactor. Move logging invocations into their own class.
// TODO: Refactor. Move currentLogUnit management into separate class.
@@ -138,15 +132,18 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// FEEDBACK_WORD_BUFFER_SIZE should add 1 because it must also hold the feedback LogUnit itself.
public static final int FEEDBACK_WORD_BUFFER_SIZE = (Integer.MAX_VALUE - 1) + 1;
+ // The special output text to invoke a research feedback dialog.
+ 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 MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel";
- private static final long RESEARCHLOG_CLOSE_TIMEOUT_IN_MS = 5 * 1000;
- private static final long RESEARCHLOG_ABORT_TIMEOUT_IN_MS = 5 * 1000;
- private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
- private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS;
+ private static final long RESEARCHLOG_CLOSE_TIMEOUT_IN_MS = TimeUnit.SECONDS.toMillis(5);
+ private static final long RESEARCHLOG_ABORT_TIMEOUT_IN_MS = TimeUnit.SECONDS.toMillis(5);
+ private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = TimeUnit.DAYS.toMillis(1);
+ private static final long MAX_LOGFILE_AGE_IN_MS = TimeUnit.DAYS.toMillis(4);
private static final ResearchLogger sInstance = new ResearchLogger();
private static String sAccountType = null;
@@ -184,8 +181,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private final MotionEventReader mMotionEventReader = new MotionEventReader();
private final Replayer mReplayer = Replayer.getInstance();
private ResearchLogDirectory mResearchLogDirectory;
+ private SplashScreen mSplashScreen;
- private Intent mUploadIntent;
private Intent mUploadNowIntent;
/* package for test */ LogUnit mCurrentLogUnit = new LogUnit();
@@ -200,7 +197,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
// not performed on text that the user types into the feedback dialog.
private boolean mInFeedbackDialog = false;
private Handler mUserRecordingTimeoutHandler;
- private static final long USER_RECORDING_TIMEOUT_MS = 30L * DateUtils.SECOND_IN_MILLIS;
+ private static final long USER_RECORDING_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(30);
// Stores a temporary LogUnit while generating a phantom space. Needed because phantom spaces
// are issued out-of-order, immediately before the characters generated by other operations that
@@ -238,7 +235,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
resetLogBuffers();
// Initialize external services
- mUploadIntent = new Intent(mLatinIME, UploaderService.class);
mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true);
if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -301,62 +297,19 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
}
- private Dialog mSplashDialog = null;
-
private void maybeShowSplashScreen() {
- if (ResearchSettings.readHasSeenSplash(mPrefs)) {
- return;
- }
- if (mSplashDialog != null && mSplashDialog.isShowing()) {
- return;
- }
- final IBinder windowToken = mMainKeyboardView != null
- ? mMainKeyboardView.getWindowToken() : null;
- if (windowToken == null) {
- return;
- }
- final AlertDialog.Builder builder = new AlertDialog.Builder(mLatinIME)
- .setTitle(R.string.research_splash_title)
- .setMessage(R.string.research_splash_content)
- .setPositiveButton(android.R.string.yes,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- onUserLoggingConsent();
- mSplashDialog.dismiss();
- }
- })
- .setNegativeButton(android.R.string.no,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- final String packageName = mLatinIME.getPackageName();
- final Uri packageUri = Uri.parse("package:" + packageName);
- final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE,
- packageUri);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mLatinIME.startActivity(intent);
- }
- })
- .setCancelable(true)
- .setOnCancelListener(
- new OnCancelListener() {
- @Override
- public void onCancel(DialogInterface dialog) {
- mLatinIME.requestHideSelf(0);
- }
- });
- mSplashDialog = builder.create();
- final Window w = mSplashDialog.getWindow();
- final WindowManager.LayoutParams lp = w.getAttributes();
- lp.token = windowToken;
- lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
- w.setAttributes(lp);
- w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
- mSplashDialog.show();
- }
-
- public void onUserLoggingConsent() {
+ if (ResearchSettings.readHasSeenSplash(mPrefs)) return;
+ if (mSplashScreen != null && mSplashScreen.isShowing()) return;
+ if (mMainKeyboardView == null) return;
+ final IBinder windowToken = mMainKeyboardView.getWindowToken();
+ if (windowToken == null) return;
+
+ mSplashScreen = new SplashScreen(mLatinIME, this);
+ mSplashScreen.showSplashScreen(windowToken);
+ }
+
+ @Override
+ public void onSplashScreenUserClickedOk() {
if (mPrefs == null) {
mPrefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME);
if (mPrefs == null) return;
@@ -367,12 +320,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
restart();
}
- private void setLoggingAllowed(final boolean enableLogging) {
- if (mPrefs == null) return;
- sIsLogging = enableLogging;
- ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, enableLogging);
- }
-
private void checkForEmptyEditor() {
if (mLatinIME == null) {
return;
@@ -429,6 +376,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mMainResearchLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
resetLogBuffers();
+ cancelFeedbackDialog();
}
public void abort() {
@@ -457,6 +405,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
}
public void onResearchKeySelected(final LatinIME latinIME) {
+ mCurrentLogUnit.removeResearchButtonInvocation();
if (mInFeedbackDialog) {
Toast.makeText(latinIME, R.string.research_please_exit_feedback_form,
Toast.LENGTH_LONG).show();
@@ -465,6 +414,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
presentFeedbackDialog(latinIME);
}
+ public void presentFeedbackDialogFromSettings() {
+ if (mLatinIME != null) {
+ presentFeedbackDialog(mLatinIME);
+ }
+ }
+
public void presentFeedbackDialog(final LatinIME latinIME) {
if (isMakingUserRecording()) {
saveRecording();
@@ -583,8 +538,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
toast.show();
boolean isLogDeleted = abort();
final long currentTime = System.currentTimeMillis();
- final long resumeTime = currentTime + 1000 * 60 *
- SUSPEND_DURATION_IN_MINUTES;
+ final long resumeTime = currentTime
+ + TimeUnit.MINUTES.toMillis(SUSPEND_DURATION_IN_MINUTES);
suspendLoggingUntil(resumeTime);
toast.cancel();
Toast.makeText(latinIME, R.string.research_notify_logging_suspended,
@@ -676,7 +631,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mMotionEventReader.readMotionEventData(mUserRecordingFile);
mReplayer.replay(replayData, null);
}
- }, 1000);
+ }, TimeUnit.SECONDS.toMillis(1));
}
if (FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD) {
@@ -701,13 +656,19 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
mInFeedbackDialog = false;
}
+ private void cancelFeedbackDialog() {
+ if (isMakingUserRecording()) {
+ cancelRecording();
+ }
+ mInFeedbackDialog = false;
+ }
+
public void initSuggest(final Suggest suggest) {
mSuggest = suggest;
// MainLogBuffer now has an out-of-date Suggest object. Close down MainLogBuffer and create
// a new one.
if (mMainLogBuffer != null) {
- stop();
- start();
+ restart();
}
}
@@ -1109,22 +1070,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
new LogStatement("MotionEvent", true, false, "action",
LogStatement.KEY_IS_LOGGING_RELATED, "motionEvent");
- public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action,
- final long eventTime, final int index, final int id, final int x, final int y) {
- if (me != null) {
- final String actionString = LoggingUtils.getMotionEventActionTypeString(action);
- final ResearchLogger researchLogger = getInstance();
- researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
- actionString, false /* IS_LOGGING_RELATED */, MotionEvent.obtain(me));
- if (action == MotionEvent.ACTION_DOWN) {
- // Subtract 1 from eventTime so the down event is included in the later
- // LogUnit, not the earlier (the test is for inequality).
- researchLogger.setSavedDownEventTime(eventTime - 1);
- }
- // Refresh the timer in case we are capturing user feedback.
- if (researchLogger.isMakingUserRecording()) {
- researchLogger.resetRecordingTimer();
- }
+ public static void mainKeyboardView_processMotionEvent(final MotionEvent me) {
+ if (me == null) {
+ return;
+ }
+ final int action = me.getActionMasked();
+ final long eventTime = me.getEventTime();
+ final String actionString = LoggingUtils.getMotionEventActionTypeString(action);
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
+ actionString, false /* IS_LOGGING_RELATED */, MotionEvent.obtain(me));
+ if (action == MotionEvent.ACTION_DOWN) {
+ // Subtract 1 from eventTime so the down event is included in the later
+ // LogUnit, not the earlier (the test is for inequality).
+ researchLogger.setSavedDownEventTime(eventTime - 1);
+ }
+ // Refresh the timer in case we are capturing user feedback.
+ if (researchLogger.isMakingUserRecording()) {
+ researchLogger.resetRecordingTimer();
}
}
@@ -1255,7 +1218,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final RichInputConnection connection) {
String word = "";
if (connection != null) {
- Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
+ TextRange range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
if (range != null) {
word = range.mWord.toString();
}
@@ -1295,10 +1258,24 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
*/
private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY =
new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index",
- "suggestion", "x", "y", "isBatchMode");
+ "suggestion", "x", "y", "isBatchMode", "score", "kind", "sourceDict");
+ /**
+ * Log a call to LatinIME.pickSuggestionManually().
+ *
+ * @param replacedWord the typed word that this manual suggestion replaces. May not be null.
+ * @param index the index in the suggestion strip
+ * @param suggestion the committed suggestion. May not be null.
+ * @param isBatchMode whether this was input in batch mode, aka gesture.
+ * @param score the internal score of the suggestion, as output by the dictionary
+ * @param kind the kind of suggestion, as one of the SuggestedWordInfo#KIND_* constants
+ * @param sourceDict the source origin of this word, as one of the Dictionary#TYPE_* constants.
+ */
public static void latinIME_pickSuggestionManually(final String replacedWord,
- final int index, final String suggestion, final boolean isBatchMode) {
+ final int index, final String suggestion, final boolean isBatchMode,
+ final int score, final int kind, final String sourceDict) {
final ResearchLogger researchLogger = getInstance();
+ // Note : suggestion can't be null here, because it's only called in a place where it
+ // can't be null.
if (!replacedWord.equals(suggestion.toString())) {
// The user chose something other than what was already there.
researchLogger.setCurrentLogUnitContainsUserDeletions();
@@ -1307,8 +1284,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
final String scrubbedWord = scrubDigitsFromString(suggestion);
researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY,
scrubDigitsFromString(replacedWord), index,
- suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE,
- Constants.SUGGESTION_STRIP_COORDINATE, isBatchMode);
+ scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE,
+ Constants.SUGGESTION_STRIP_COORDINATE, isBatchMode, score, kind, sourceDict);
researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode);
researchLogger.mStatistics.recordManualSuggestion(SystemClock.uptimeMillis());
}
@@ -1437,7 +1414,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
"navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled",
"hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th",
"keys");
- public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) {
+ public static void mainKeyboardView_setKeyboard(final Keyboard keyboard,
+ final int orientation) {
final KeyboardId kid = keyboard.mId;
final boolean isPasswordView = kid.passwordInput();
final ResearchLogger researchLogger = getInstance();
@@ -1445,11 +1423,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
researchLogger.enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD,
KeyboardId.elementIdToName(kid.mElementId),
kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
- kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(),
+ orientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(),
kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey,
isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey,
kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth,
- keyboard.mOccupiedHeight, keyboard.mKeys);
+ keyboard.mOccupiedHeight, keyboard.getKeys());
}
/**
@@ -1533,16 +1511,9 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
Constants.printableCode(scrubDigitFromCodePoint(code)),
outputText == null ? null : scrubDigitsFromString(outputText.toString()),
x, y, ignoreModifierKey, altersCode, key.isEnabled());
- if (code == Constants.CODE_RESEARCH) {
- researchLogger.suppressResearchKeyMotionData();
- }
}
}
- private void suppressResearchKeyMotionData() {
- mCurrentLogUnit.removeResearchButtonInvocation();
- }
-
/**
* Log a call to PointerTracker.callListenerCallListenerOnRelease().
*
@@ -1794,7 +1765,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
if (me != null) {
getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT,
- me.toString());
+ MotionEvent.obtain(me));
}
}
@@ -1830,7 +1801,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
*/
private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT =
new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText",
- "enteredWordPos");
+ "enteredWordPos", "suggestedWords");
public static void latinIME_onEndBatchInput(final CharSequence enteredText,
final int enteredWordPos, final SuggestedWords suggestedWords) {
final ResearchLogger researchLogger = getInstance();
@@ -1838,7 +1809,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang
researchLogger.mCurrentLogUnit.setWords(enteredText.toString());
}
researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText,
- enteredWordPos);
+ enteredWordPos, suggestedWords);
researchLogger.mCurrentLogUnit.initializeSuggestions(suggestedWords);
researchLogger.mStatistics.recordGestureInput(enteredText.length(),
SystemClock.uptimeMillis());
diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java
index e573ca012..fd323a104 100644
--- a/java/src/com/android/inputmethod/research/Statistics.java
+++ b/java/src/com/android/inputmethod/research/Statistics.java
@@ -21,6 +21,8 @@ import android.util.Log;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.define.ProductionFlag;
+import java.util.concurrent.TimeUnit;
+
public class Statistics {
private static final String TAG = Statistics.class.getSimpleName();
private static final boolean DEBUG = false
@@ -102,8 +104,8 @@ public class Statistics {
// To account for the interruptions when the user's attention is directed elsewhere, times
// longer than MIN_TYPING_INTERMISSION are not counted when estimating this statistic.
- public static final int MIN_TYPING_INTERMISSION = 2 * 1000; // in milliseconds
- public static final int MIN_DELETION_INTERMISSION = 10 * 1000; // in milliseconds
+ public static final long MIN_TYPING_INTERMISSION = TimeUnit.SECONDS.toMillis(2);
+ public static final long MIN_DELETION_INTERMISSION = TimeUnit.SECONDS.toMillis(10);
// The last time that a tap was performed
private long mLastTapTime;
diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java
index d2db34927..fd3f2f60e 100644
--- a/java/src/com/android/inputmethod/research/UploaderService.java
+++ b/java/src/com/android/inputmethod/research/UploaderService.java
@@ -24,8 +24,6 @@ import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
-import com.android.inputmethod.latin.define.ProductionFlag;
-
/**
* Service to invoke the uploader.
*
@@ -33,12 +31,9 @@ import com.android.inputmethod.latin.define.ProductionFlag;
*/
public final class UploaderService extends IntentService {
private static final String TAG = UploaderService.class.getSimpleName();
- private static final boolean DEBUG = false
- && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR;
public static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
+ ".extra.UPLOAD_UNCONDITIONALLY";
- protected static final int TIMEOUT_IN_MS = 1000 * 4;
public UploaderService() {
super("Research Uploader Service");
diff --git a/java/src/com/android/inputmethod/research/ui/SplashScreen.java b/java/src/com/android/inputmethod/research/ui/SplashScreen.java
new file mode 100644
index 000000000..78ed668d1
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/ui/SplashScreen.java
@@ -0,0 +1,111 @@
+/*
+ * 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.research.ui;
+
+import android.app.AlertDialog.Builder;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.inputmethodservice.InputMethodService;
+import android.net.Uri;
+import android.os.IBinder;
+import android.view.Window;
+import android.view.WindowManager.LayoutParams;
+
+import com.android.inputmethod.latin.R.string;
+
+/**
+ * Show a dialog when the user first opens the keyboard.
+ *
+ * The splash screen is a modal dialog box presented when the user opens this keyboard for the first
+ * time. It is useful for giving specific warnings that must be shown to the user before use.
+ *
+ * While the splash screen does share with the setup wizard the common goal of presenting
+ * information to the user before use, they are presented at different times and with different
+ * capabilities. The setup wizard is launched by tapping on the icon, and walks the user through
+ * the setup process. It can, however, be bypassed by enabling the keyboard from Settings directly.
+ * The splash screen cannot be bypassed, and is therefore more appropriate for obtaining user
+ * consent.
+ */
+public class SplashScreen {
+ public interface UserConsentListener {
+ public void onSplashScreenUserClickedOk();
+ }
+
+ final UserConsentListener mListener;
+ final Dialog mSplashDialog;
+
+ public SplashScreen(final InputMethodService inputMethodService,
+ final UserConsentListener listener) {
+ mListener = listener;
+ final Builder builder = new Builder(inputMethodService)
+ .setTitle(string.research_splash_title)
+ .setMessage(string.research_splash_content)
+ .setPositiveButton(android.R.string.yes,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mListener.onSplashScreenUserClickedOk();
+ mSplashDialog.dismiss();
+ }
+ })
+ .setNegativeButton(android.R.string.no,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final String packageName = inputMethodService.getPackageName();
+ final Uri packageUri = Uri.parse("package:" + packageName);
+ final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE,
+ packageUri);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ inputMethodService.startActivity(intent);
+ }
+ })
+ .setCancelable(true)
+ .setOnCancelListener(
+ new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ inputMethodService.requestHideSelf(0);
+ }
+ });
+ mSplashDialog = builder.create();
+ }
+
+ /**
+ * Show the splash screen.
+ *
+ * The user must consent to the terms presented in the SplashScreen before they can use the
+ * keyboard. If they cancel instead, they are given the option to uninstall the keybard.
+ *
+ * @param windowToken {@link IBinder} to attach dialog to
+ */
+ public void showSplashScreen(final IBinder windowToken) {
+ final Window window = mSplashDialog.getWindow();
+ final LayoutParams lp = window.getAttributes();
+ lp.token = windowToken;
+ lp.type = LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
+ window.setAttributes(lp);
+ window.addFlags(LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+ mSplashDialog.show();
+ }
+
+ public boolean isShowing() {
+ return mSplashDialog.isShowing();
+ }
+}