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.java14
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java35
-rw-r--r--java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java45
-rw-r--r--java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java22
-rw-r--r--java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java2
-rw-r--r--java/src/com/android/inputmethod/compat/CompatUtils.java2
-rw-r--r--java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java2
-rw-r--r--java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java2
-rw-r--r--java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java34
-rw-r--r--java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java2
-rw-r--r--java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java16
-rw-r--r--java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/Key.java311
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyDetector.java56
-rw-r--r--java/src/com/android/inputmethod/keyboard/Keyboard.java1184
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java51
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardId.java37
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java110
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java102
-rw-r--r--java/src/com/android/inputmethod/keyboard/KeyboardView.java853
-rw-r--r--java/src/com/android/inputmethod/keyboard/MainKeyboardView.java (renamed from java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java)396
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java434
-rw-r--r--java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java22
-rw-r--r--java/src/com/android/inputmethod/keyboard/PointerTracker.java787
-rw-r--r--java/src/com/android/inputmethod/keyboard/ProximityInfo.java85
-rw-r--r--java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/AlphabetShiftState.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java297
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java341
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java94
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java140
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java44
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java80
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java46
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java (renamed from java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java)108
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java130
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java813
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java11
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java13
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java137
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java154
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java21
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java1072
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/KeysCache.java40
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java86
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java174
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java348
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java2
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java (renamed from java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java)14
-rw-r--r--java/src/com/android/inputmethod/keyboard/internal/TouchPositionCorrection.java95
-rw-r--r--java/src/com/android/inputmethod/latin/AdditionalSubtype.java46
-rw-r--r--java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java105
-rw-r--r--java/src/com/android/inputmethod/latin/AssetFileAddress.java2
-rw-r--r--java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java2
-rw-r--r--java/src/com/android/inputmethod/latin/AutoCorrection.java98
-rw-r--r--java/src/com/android/inputmethod/latin/BackupAgent.java2
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionary.java213
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java13
-rw-r--r--java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java91
-rw-r--r--java/src/com/android/inputmethod/latin/BoundedTreeSet.java49
-rw-r--r--java/src/com/android/inputmethod/latin/CollectionUtils.java95
-rw-r--r--java/src/com/android/inputmethod/latin/Constants.java26
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java22
-rw-r--r--java/src/com/android/inputmethod/latin/ContactsDictionary.java178
-rw-r--r--java/src/com/android/inputmethod/latin/DebugSettings.java13
-rw-r--r--java/src/com/android/inputmethod/latin/DebugSettingsActivity.java2
-rw-r--r--java/src/com/android/inputmethod/latin/DicTraverseSession.java75
-rw-r--r--java/src/com/android/inputmethod/latin/Dictionary.java80
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryCollection.java53
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryFactory.java15
-rw-r--r--java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java2
-rw-r--r--java/src/com/android/inputmethod/latin/EditingUtils.java182
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java66
-rw-r--r--java/src/com/android/inputmethod/latin/ExpandableDictionary.java157
-rw-r--r--java/src/com/android/inputmethod/latin/FileTransforms.java2
-rw-r--r--java/src/com/android/inputmethod/latin/ImfUtils.java9
-rw-r--r--java/src/com/android/inputmethod/latin/InputAttributes.java13
-rw-r--r--java/src/com/android/inputmethod/latin/InputPointers.java133
-rw-r--r--java/src/com/android/inputmethod/latin/InputTypeUtils.java2
-rw-r--r--java/src/com/android/inputmethod/latin/InputView.java15
-rw-r--r--java/src/com/android/inputmethod/latin/JniUtils.java8
-rw-r--r--java/src/com/android/inputmethod/latin/LastComposedWord.java32
-rw-r--r--java/src/com/android/inputmethod/latin/LatinIME.java1905
-rw-r--r--java/src/com/android/inputmethod/latin/LatinImeLogger.java11
-rw-r--r--java/src/com/android/inputmethod/latin/LocaleUtils.java41
-rw-r--r--java/src/com/android/inputmethod/latin/ResearchLogger.java757
-rw-r--r--java/src/com/android/inputmethod/latin/ResizableIntArray.java146
-rw-r--r--java/src/com/android/inputmethod/latin/ResourceUtils.java128
-rw-r--r--java/src/com/android/inputmethod/latin/RichInputConnection.java709
-rw-r--r--java/src/com/android/inputmethod/latin/Settings.java188
-rw-r--r--java/src/com/android/inputmethod/latin/SettingsActivity.java2
-rw-r--r--java/src/com/android/inputmethod/latin/SettingsValues.java221
-rw-r--r--java/src/com/android/inputmethod/latin/StringUtils.java233
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeLocale.java12
-rw-r--r--java/src/com/android/inputmethod/latin/SubtypeSwitcher.java100
-rw-r--r--java/src/com/android/inputmethod/latin/Suggest.java589
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestedWords.java77
-rw-r--r--java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java2
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java13
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java54
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java12
-rw-r--r--java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java56
-rw-r--r--java/src/com/android/inputmethod/latin/TargetApplicationGetter.java4
-rw-r--r--java/src/com/android/inputmethod/latin/UserBinaryDictionary.java47
-rw-r--r--java/src/com/android/inputmethod/latin/UserDictionary.java225
-rw-r--r--java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java214
-rw-r--r--java/src/com/android/inputmethod/latin/UserHistoryDictionary.java513
-rw-r--r--java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java19
-rw-r--r--java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java22
-rw-r--r--java/src/com/android/inputmethod/latin/Utils.java165
-rw-r--r--java/src/com/android/inputmethod/latin/VibratorUtils.java2
-rw-r--r--java/src/com/android/inputmethod/latin/WhitelistDictionary.java109
-rw-r--r--java/src/com/android/inputmethod/latin/WordComposer.java179
-rw-r--r--java/src/com/android/inputmethod/latin/WordListInfo.java2
-rw-r--r--java/src/com/android/inputmethod/latin/XmlParseUtils.java10
-rw-r--r--java/src/com/android/inputmethod/latin/define/ProductionFlag.java1
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java282
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java1120
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java8
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FormatSpec.java258
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java136
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/MakedictLog.java2
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java2
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java2
-rw-r--r--java/src/com/android/inputmethod/latin/makedict/Word.java25
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java502
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java154
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java25
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java326
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java2
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java61
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java16
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java2
-rw-r--r--java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java2
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java276
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java8
-rw-r--r--java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java (renamed from java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java)132
-rw-r--r--java/src/com/android/inputmethod/research/BootBroadcastReceiver.java33
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackActivity.java54
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackFragment.java73
-rw-r--r--java/src/com/android/inputmethod/research/FeedbackLayout.java62
-rw-r--r--java/src/com/android/inputmethod/research/LogBuffer.java113
-rw-r--r--java/src/com/android/inputmethod/research/LogUnit.java83
-rw-r--r--java/src/com/android/inputmethod/research/MainLogBuffer.java127
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLog.java320
-rw-r--r--java/src/com/android/inputmethod/research/ResearchLogger.java1315
-rw-r--r--java/src/com/android/inputmethod/research/Statistics.java146
-rw-r--r--java/src/com/android/inputmethod/research/UploaderService.java191
149 files changed, 14856 insertions, 8443 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
index 70e38fdb0..b9b6362fc 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityEntityProvider.java
@@ -35,6 +35,7 @@ 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;
/**
* Exposes a virtual view sub-tree for {@link KeyboardView} and generates
@@ -46,7 +47,7 @@ import com.android.inputmethod.keyboard.KeyboardView;
* virtual views, thus conveying their logical structure.
* </p>
*/
-public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat {
+public final class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat {
private static final String TAG = AccessibilityEntityProvider.class.getSimpleName();
private static final int UNDEFINED = Integer.MIN_VALUE;
@@ -55,7 +56,7 @@ public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat
private final AccessibilityUtils mAccessibilityUtils;
/** A map of integer IDs to {@link Key}s. */
- private final SparseArray<Key> mVirtualViewIdToKey = new SparseArray<Key>();
+ private final SparseArray<Key> mVirtualViewIdToKey = CollectionUtils.newSparseArray();
/** Temporary rect used to calculate in-screen bounds. */
private final Rect mTempBoundsInScreen = new Rect();
@@ -195,8 +196,7 @@ public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat
info.setSource(mKeyboardView, virtualViewId);
info.setBoundsInScreen(boundsInScreen);
info.setEnabled(true);
- info.setClickable(true);
- info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
+ info.setVisibleToUser(true);
if (mAccessibilityFocusedView == virtualViewId) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
@@ -225,6 +225,9 @@ public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat
mKeyboardView.onTouchEvent(downEvent);
mKeyboardView.onTouchEvent(upEvent);
+
+ downEvent.recycle();
+ upEvent.recycle();
}
@Override
@@ -251,9 +254,6 @@ public class AccessibilityEntityProvider extends AccessibilityNodeProviderCompat
final int virtualViewId = generateVirtualViewIdForKey(key);
switch (action) {
- case AccessibilityNodeInfoCompat.ACTION_CLICK:
- simulateKeyPress(key);
- return true;
case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
if (mAccessibilityFocusedView == virtualViewId) {
return false;
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
index 616b1c6d7..1eee1df87 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java
@@ -19,10 +19,15 @@ package com.android.inputmethod.accessibility;
import android.content.Context;
import android.inputmethodservice.InputMethodService;
import android.media.AudioManager;
+import android.os.Build;
import android.os.SystemClock;
import android.provider.Settings;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.util.Log;
import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.inputmethod.EditorInfo;
@@ -32,7 +37,7 @@ import com.android.inputmethod.compat.SettingsSecureCompatUtils;
import com.android.inputmethod.latin.InputTypeUtils;
import com.android.inputmethod.latin.R;
-public class AccessibilityUtils {
+public final class AccessibilityUtils {
private static final String TAG = AccessibilityUtils.class.getSimpleName();
private static final String CLASS = AccessibilityUtils.class.getClass().getName();
private static final String PACKAGE = AccessibilityUtils.class.getClass().getPackage()
@@ -138,9 +143,10 @@ public class AccessibilityUtils {
* Sends the specified text to the {@link AccessibilityManager} to be
* spoken.
*
- * @param text the text to speak
+ * @param view The source view.
+ * @param text The text to speak.
*/
- public void speak(CharSequence text) {
+ public void announceForAccessibility(View view, CharSequence text) {
if (!mAccessibilityManager.isEnabled()) {
Log.e(TAG, "Attempted to speak when accessibility was disabled!");
return;
@@ -149,8 +155,7 @@ public class AccessibilityUtils {
// The following is a hack to avoid using the heavy-weight TextToSpeech
// class. Instead, we're just forcing a fake AccessibilityEvent into
// the screen reader to make it speak.
- final AccessibilityEvent event = AccessibilityEvent
- .obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ final AccessibilityEvent event = AccessibilityEvent.obtain();
event.setPackageName(PACKAGE);
event.setClassName(CLASS);
@@ -158,20 +163,34 @@ public class AccessibilityUtils {
event.setEnabled(true);
event.getText().add(text);
- mAccessibilityManager.sendAccessibilityEvent(event);
+ // Platforms starting at SDK 16 should use announce events.
+ if (Build.VERSION.SDK_INT >= 16) {
+ event.setEventType(AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
+ } else {
+ event.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+ }
+
+ final ViewParent viewParent = view.getParent();
+ if ((viewParent == null) || !(viewParent instanceof ViewGroup)) {
+ Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility");
+ return;
+ }
+
+ viewParent.requestSendAccessibilityEvent(view, event);
}
/**
* Handles speaking the "connect a headset to hear passwords" notification
* when connecting to a password field.
*
+ * @param view The source view.
* @param editorInfo The input connection's editor info attribute.
* @param restarting Whether the connection is being restarted.
*/
- public void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) {
+ public void onStartInputViewInternal(View view, EditorInfo editorInfo, boolean restarting) {
if (shouldObscureInput(editorInfo)) {
final CharSequence text = mContext.getText(R.string.spoken_use_headphones);
- speak(text);
+ announceForAccessibility(view, text);
}
}
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
index f6376d5f4..fcfa6d4e4 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -24,27 +24,26 @@ import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewConfiguration;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardId;
-import com.android.inputmethod.keyboard.LatinKeyboardView;
+import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.keyboard.PointerTracker;
import com.android.inputmethod.latin.R;
-public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
+public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy();
private InputMethodService mInputMethod;
- private LatinKeyboardView mView;
+ private MainKeyboardView mView;
private AccessibilityEntityProvider mAccessibilityNodeProvider;
private Key mLastHoverKey = null;
/**
* Inset in pixels to look for keys when the user's finger exits the
- * keyboard area. See {@link ViewConfiguration#getScaledEdgeSlop()}.
+ * keyboard area.
*/
private int mEdgeSlop;
@@ -62,7 +61,8 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
private void initInternal(InputMethodService inputMethod) {
mInputMethod = inputMethod;
- mEdgeSlop = ViewConfiguration.get(inputMethod).getScaledEdgeSlop();
+ mEdgeSlop = inputMethod.getResources().getDimensionPixelSize(
+ R.dimen.accessibility_edge_slop);
}
/**
@@ -70,7 +70,7 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
*
* @param view The view to wrap.
*/
- public void setView(LatinKeyboardView view) {
+ public void setView(MainKeyboardView view) {
if (view == null) {
// Ignore null views.
return;
@@ -105,8 +105,21 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
}
/**
- * Receives hover events when accessibility is turned on in SDK versions ICS
- * and higher.
+ * Intercepts touch events before dispatch when touch exploration is turned
+ * on in ICS and higher.
+ *
+ * @param event The motion event being dispatched.
+ * @return {@code true} if the event is handled
+ */
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ // To avoid accidental key presses during touch exploration, always drop
+ // touch events generated by the user.
+ return false;
+ }
+
+ /**
+ * Receives hover events when touch exploration is turned on in SDK versions
+ * ICS and higher.
*
* @param event The hover event.
* @return {@code true} if the event is handled
@@ -114,8 +127,14 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
public boolean dispatchHoverEvent(MotionEvent event, PointerTracker tracker) {
final int x = (int) event.getX();
final int y = (int) event.getY();
- final Key key = tracker.getKeyOn(x, y);
final Key previousKey = mLastHoverKey;
+ final Key key;
+
+ if (pointInView(x, y)) {
+ key = tracker.getKeyOn(x, y);
+ } else {
+ key = null;
+ }
mLastHoverKey = key;
@@ -123,7 +142,7 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
case MotionEvent.ACTION_HOVER_EXIT:
// Make sure we're not getting an EXIT event because the user slid
// off the keyboard area, then force a key press.
- if (pointInView(x, y) && (key != null)) {
+ if (key != null) {
getAccessibilityNodeProvider().simulateKeyPress(key);
}
//$FALL-THROUGH$
@@ -250,7 +269,7 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
text = context.getText(R.string.spoken_description_shiftmode_off);
}
- AccessibilityUtils.getInstance().speak(text);
+ AccessibilityUtils.getInstance().announceForAccessibility(mView, text);
}
/**
@@ -290,6 +309,6 @@ public class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat {
}
final String text = context.getString(resId);
- AccessibilityUtils.getInstance().speak(text);
+ AccessibilityUtils.getInstance().announceForAccessibility(mView, text);
}
}
diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
index 23acb8b74..32618ad85 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java
@@ -19,16 +19,18 @@ package com.android.inputmethod.accessibility;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
+import android.util.SparseIntArray;
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.R;
import java.util.HashMap;
-public class KeyCodeDescriptionMapper {
+public final class KeyCodeDescriptionMapper {
private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName();
// The resource ID of the string spoken for obscured keys
@@ -37,10 +39,10 @@ public class KeyCodeDescriptionMapper {
private static KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
// Map of key labels to spoken description resource IDs
- private final HashMap<CharSequence, Integer> mKeyLabelMap;
+ private final HashMap<CharSequence, Integer> mKeyLabelMap = CollectionUtils.newHashMap();
- // Map of key codes to spoken description resource IDs
- private final HashMap<Integer, Integer> mKeyCodeMap;
+ // Sparse array of spoken description resource IDs indexed by key codes
+ private final SparseIntArray mKeyCodeMap;
public static void init() {
sInstance.initInternal();
@@ -51,18 +53,15 @@ public class KeyCodeDescriptionMapper {
}
private KeyCodeDescriptionMapper() {
- mKeyLabelMap = new HashMap<CharSequence, Integer>();
- mKeyCodeMap = new HashMap<Integer, Integer>();
+ mKeyCodeMap = new SparseIntArray();
}
private void initInternal() {
// Manual label substitutions for key labels with no string resource
mKeyLabelMap.put(":-)", R.string.spoken_description_smiley);
- // Symbols that most TTS engines can't speak
- mKeyCodeMap.put((int) ' ', R.string.spoken_description_space);
-
// Special non-character codes defined in Keyboard
+ mKeyCodeMap.put(Keyboard.CODE_SPACE, R.string.spoken_description_space);
mKeyCodeMap.put(Keyboard.CODE_DELETE, R.string.spoken_description_delete);
mKeyCodeMap.put(Keyboard.CODE_ENTER, R.string.spoken_description_return);
mKeyCodeMap.put(Keyboard.CODE_SETTINGS, R.string.spoken_description_settings);
@@ -70,6 +69,9 @@ public class KeyCodeDescriptionMapper {
mKeyCodeMap.put(Keyboard.CODE_SHORTCUT, R.string.spoken_description_mic);
mKeyCodeMap.put(Keyboard.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol);
mKeyCodeMap.put(Keyboard.CODE_TAB, R.string.spoken_description_tab);
+ mKeyCodeMap.put(Keyboard.CODE_LANGUAGE_SWITCH, R.string.spoken_description_language_switch);
+ mKeyCodeMap.put(Keyboard.CODE_ACTION_NEXT, R.string.spoken_description_action_next);
+ mKeyCodeMap.put(Keyboard.CODE_ACTION_PREVIOUS, R.string.spoken_description_action_previous);
}
/**
@@ -273,7 +275,7 @@ public class KeyCodeDescriptionMapper {
return context.getString(OBSCURED_KEY_RES_ID);
}
- if (mKeyCodeMap.containsKey(code)) {
+ if (mKeyCodeMap.indexOfKey(code) >= 0) {
return context.getString(mKeyCodeMap.get(code));
} else if (isDefinedNonCtrl) {
return Character.toString((char) code);
diff --git a/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java b/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java
index b6c3e2a88..40eed91f2 100644
--- a/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java
+++ b/java/src/com/android/inputmethod/compat/AudioManagerCompatWrapper.java
@@ -20,7 +20,7 @@ import android.media.AudioManager;
import java.lang.reflect.Method;
-public class AudioManagerCompatWrapper {
+public final class AudioManagerCompatWrapper {
private static final Method METHOD_isWiredHeadsetOn = CompatUtils.getMethod(
AudioManager.class, "isWiredHeadsetOn");
private static final Method METHOD_isBluetoothA2dpOn = CompatUtils.getMethod(
diff --git a/java/src/com/android/inputmethod/compat/CompatUtils.java b/java/src/com/android/inputmethod/compat/CompatUtils.java
index ce427e9c9..ffed6ecb1 100644
--- a/java/src/com/android/inputmethod/compat/CompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/CompatUtils.java
@@ -24,7 +24,7 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
-public class CompatUtils {
+public final class CompatUtils {
private static final String TAG = CompatUtils.class.getSimpleName();
private static final String EXTRA_INPUT_METHOD_ID = "input_method_id";
// TODO: Can these be constants instead of literal String constants?
diff --git a/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java
index 08c246f8b..210058bec 100644
--- a/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/EditorInfoCompatUtils.java
@@ -20,7 +20,7 @@ import android.view.inputmethod.EditorInfo;
import java.lang.reflect.Field;
-public class EditorInfoCompatUtils {
+public final class EditorInfoCompatUtils {
// EditorInfo.IME_FLAG_FORCE_ASCII has been introduced since API#16 (JellyBean).
private static final Field FIELD_IME_FLAG_FORCE_ASCII = CompatUtils.getField(
EditorInfo.class, "IME_FLAG_FORCE_ASCII");
diff --git a/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java b/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java
index cc10a4ed2..a01c301ee 100644
--- a/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java
+++ b/java/src/com/android/inputmethod/compat/InputMethodManagerCompatWrapper.java
@@ -27,7 +27,7 @@ import java.lang.reflect.Method;
// TODO: Override this class with the concrete implementation if we need to take care of the
// performance.
-public class InputMethodManagerCompatWrapper {
+public final class InputMethodManagerCompatWrapper {
private static final String TAG = InputMethodManagerCompatWrapper.class.getSimpleName();
private static final Method METHOD_switchToNextInputMethod = CompatUtils.getMethod(
InputMethodManager.class, "switchToNextInputMethod", IBinder.class, Boolean.TYPE);
diff --git a/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java
new file mode 100644
index 000000000..8eea31ed2
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/InputMethodServiceCompatUtils.java
@@ -0,0 +1,34 @@
+/*
+ * 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.compat;
+
+import android.inputmethodservice.InputMethodService;
+
+import java.lang.reflect.Method;
+
+public final class InputMethodServiceCompatUtils {
+ private static final Method METHOD_enableHardwareAcceleration =
+ CompatUtils.getMethod(InputMethodService.class, "enableHardwareAcceleration");
+
+ private InputMethodServiceCompatUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static boolean enableHardwareAcceleration(InputMethodService ims) {
+ return (Boolean)CompatUtils.invoke(ims, false, METHOD_enableHardwareAcceleration);
+ }
+}
diff --git a/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java b/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java
index 1b79992f0..db5abd0fe 100644
--- a/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/SettingsSecureCompatUtils.java
@@ -18,7 +18,7 @@ package com.android.inputmethod.compat;
import java.lang.reflect.Field;
-public class SettingsSecureCompatUtils {
+public final class SettingsSecureCompatUtils {
private static final Field FIELD_ACCESSIBILITY_SPEAK_PASSWORD = CompatUtils.getField(
android.provider.Settings.Secure.class, "ACCESSIBILITY_SPEAK_PASSWORD");
diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
index a0f48d24c..159f43650 100644
--- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
+++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
@@ -16,10 +16,6 @@
package com.android.inputmethod.compat;
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.SuggestedWords;
-import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver;
-
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
@@ -27,12 +23,17 @@ import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;
+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 java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Locale;
-public class SuggestionSpanUtils {
+public final class SuggestionSpanUtils {
private static final String TAG = SuggestionSpanUtils.class.getSimpleName();
// TODO: Use reflection to get field values
public static final String ACTION_SUGGESTION_PICKED =
@@ -119,8 +120,7 @@ public class SuggestionSpanUtils {
} else {
spannable = new SpannableString(pickedWord);
}
- final ArrayList<String> suggestionsList = new ArrayList<String>();
- boolean sameAsTyped = false;
+ final ArrayList<String> suggestionsList = CollectionUtils.newArrayList();
for (int i = 0; i < suggestedWords.size(); ++i) {
if (suggestionsList.size() >= OBJ_SUGGESTIONS_MAX_SIZE) {
break;
@@ -128,8 +128,6 @@ public class SuggestionSpanUtils {
final CharSequence word = suggestedWords.getWord(i);
if (!TextUtils.equals(pickedWord, word)) {
suggestionsList.add(word.toString());
- } else if (i == 0) {
- sameAsTyped = true;
}
}
diff --git a/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java b/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java
index e5f9db27c..8314212c9 100644
--- a/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/SuggestionsInfoCompatUtils.java
@@ -20,7 +20,7 @@ import android.view.textservice.SuggestionsInfo;
import java.lang.reflect.Field;
-public class SuggestionsInfoCompatUtils {
+public final class SuggestionsInfoCompatUtils {
private static final Field FIELD_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = CompatUtils.getField(
SuggestionsInfo.class, "RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS");
private static final Integer OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = (Integer) CompatUtils
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index e1e1ca9cf..30812e8c3 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -31,11 +31,16 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.Xml;
+import com.android.inputmethod.keyboard.internal.KeyDrawParams;
import com.android.inputmethod.keyboard.internal.KeySpecParser;
-import com.android.inputmethod.keyboard.internal.KeySpecParser.MoreKeySpec;
-import com.android.inputmethod.keyboard.internal.KeyStyles.KeyStyle;
+import com.android.inputmethod.keyboard.internal.KeyStyle;
+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.keyboard.internal.KeyboardRow;
+import com.android.inputmethod.keyboard.internal.MoreKeySpec;
import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResourceUtils;
import com.android.inputmethod.latin.StringUtils;
import org.xmlpull.v1.XmlPullParser;
@@ -47,14 +52,13 @@ import java.util.Locale;
/**
* Class for describing the position and characteristics of a single key in the keyboard.
*/
-public class Key {
+public class Key implements Comparable<Key> {
private static final String TAG = Key.class.getSimpleName();
/**
* The key code (unicode or custom code) that this key generates.
*/
public final int mCode;
- public final int mAltCode;
/** Label to display */
public final String mLabel;
@@ -89,22 +93,11 @@ public class Key {
/** Icon to display instead of a label. Icon takes precedence over a label */
private final int mIconId;
- /** Icon for disabled state */
- private final int mDisabledIconId;
- /** Preview version of the icon, for the preview popup */
- private final int mPreviewIconId;
/** Width of the key, not including the gap */
public final int mWidth;
/** Height of the key, not including the gap */
public final int mHeight;
- /** The horizontal gap around this key */
- public final int mHorizontalGap;
- /** The vertical gap below this key */
- public final int mVerticalGap;
- /** The visual insets */
- public final int mVisualInsetsLeft;
- public final int mVisualInsetsRight;
/** X coordinate of the key in the keyboard layout */
public final int mX;
/** Y coordinate of the key in the keyboard layout */
@@ -112,8 +105,6 @@ public class Key {
/** Hit bounding box of the key */
public final Rect mHitBox = new Rect();
- /** Text to output when pressed. This can be multiple characters, like ".com" */
- public final CharSequence mOutputText;
/** More keys */
public final MoreKeySpec[] mMoreKeys;
/** More keys column number and flags */
@@ -143,6 +134,34 @@ public class 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 OptionalAttributes mOptionalAttributes;
+
+ private static final class OptionalAttributes {
+ /** Text to output when pressed. This can be multiple characters, like ".com" */
+ public final String mOutputText;
+ public final int mAltCode;
+ /** Icon for disabled state */
+ public final int mDisabledIconId;
+ /** Preview version of the icon, for the preview popup */
+ public final int mPreviewIconId;
+ /** The visual insets */
+ public final int mVisualInsetsLeft;
+ public final int mVisualInsetsRight;
+
+ public OptionalAttributes(final String outputText, final int altCode,
+ final int disabledIconId, final int previewIconId,
+ final int visualInsetsLeft, final int visualInsetsRight) {
+ mOutputText = outputText;
+ mAltCode = altCode;
+ mDisabledIconId = disabledIconId;
+ mPreviewIconId = previewIconId;
+ mVisualInsetsLeft = visualInsetsLeft;
+ mVisualInsetsRight = visualInsetsRight;
+ }
+ }
+
private final int mHashCode;
/** The current pressed state of this key */
@@ -153,8 +172,8 @@ public class Key {
/**
* This constructor is being used only for keys in more keys keyboard.
*/
- public Key(Keyboard.Params params, MoreKeySpec moreKeySpec, int x, int y, int width, int height,
- int labelFlags) {
+ 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);
}
@@ -162,13 +181,11 @@ public class Key {
/**
* This constructor is being used only for key in popup suggestions pane.
*/
- public Key(Keyboard.Params params, String label, String hintLabel, int iconId,
- int code, String outputText, int x, int y, int width, int height, int labelFlags) {
+ 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) {
mHeight = height - params.mVerticalGap;
- mHorizontalGap = params.mHorizontalGap;
- mVerticalGap = params.mVerticalGap;
- mVisualInsetsLeft = mVisualInsetsRight = 0;
- mWidth = width - mHorizontalGap;
+ mWidth = width - params.mHorizontalGap;
mHintLabel = hintLabel;
mLabelFlags = labelFlags;
mBackgroundType = BACKGROUND_TYPE_NORMAL;
@@ -176,17 +193,20 @@ public class Key {
mMoreKeys = null;
mMoreKeysColumnAndFlags = 0;
mLabel = label;
- mOutputText = outputText;
+ if (outputText == null) {
+ mOptionalAttributes = null;
+ } else {
+ mOptionalAttributes = new OptionalAttributes(outputText, CODE_UNSPECIFIED,
+ ICON_UNDEFINED, ICON_UNDEFINED, 0, 0);
+ }
mCode = code;
mEnabled = (code != CODE_UNSPECIFIED);
- mAltCode = CODE_UNSPECIFIED;
mIconId = iconId;
- mDisabledIconId = ICON_UNDEFINED;
- mPreviewIconId = ICON_UNDEFINED;
// Horizontal gap is divided equally to both sides of the key.
- mX = x + mHorizontalGap / 2;
+ mX = x + params.mHorizontalGap / 2;
mY = y;
mHitBox.set(x, y, x + width + 1, y + height);
+ mKeyVisualAttributes = null;
mHashCode = computeHashCode(this);
}
@@ -201,12 +221,11 @@ public class Key {
* @param parser the XML parser containing the attributes for this key
* @throws XmlPullParserException
*/
- public Key(Resources res, Keyboard.Params params, Keyboard.Builder.Row row,
- XmlPullParser parser) throws XmlPullParserException {
+ 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 keyHeight = row.mRowHeight;
- mVerticalGap = params.mVerticalGap;
- mHeight = keyHeight - mVerticalGap;
+ mHeight = keyHeight - params.mVerticalGap;
final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard_Key);
@@ -220,7 +239,6 @@ public class Key {
mX = Math.round(keyXPos + horizontalGap / 2);
mY = keyYPos;
mWidth = Math.round(keyWidth - horizontalGap);
- mHorizontalGap = Math.round(horizontalGap);
mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1,
keyYPos + keyHeight);
// Update row to have current x coordinate.
@@ -229,15 +247,15 @@ public class Key {
mBackgroundType = style.getInt(keyAttr,
R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType());
- mVisualInsetsLeft = Math.round(Keyboard.Builder.getDimensionOrFraction(keyAttr,
+ final int visualInsetsLeft = Math.round(ResourceUtils.getDimensionOrFraction(keyAttr,
R.styleable.Keyboard_Key_visualInsetsLeft, params.mBaseWidth, 0));
- mVisualInsetsRight = Math.round(Keyboard.Builder.getDimensionOrFraction(keyAttr,
+ final int visualInsetsRight = Math.round(ResourceUtils.getDimensionOrFraction(keyAttr,
R.styleable.Keyboard_Key_visualInsetsRight, params.mBaseWidth, 0));
mIconId = KeySpecParser.getIconId(style.getString(keyAttr,
R.styleable.Keyboard_Key_keyIcon));
- mDisabledIconId = KeySpecParser.getIconId(style.getString(keyAttr,
+ final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr,
R.styleable.Keyboard_Key_keyIconDisabled));
- mPreviewIconId = KeySpecParser.getIconId(style.getString(keyAttr,
+ final int previewIconId = KeySpecParser.getIconId(style.getString(keyAttr,
R.styleable.Keyboard_Key_keyIconPreview));
mLabelFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags)
@@ -331,21 +349,28 @@ public class Key {
} else {
mCode = KeySpecParser.toUpperCaseOfCodeForLocale(code, needsToUpperCase, locale);
}
- mOutputText = outputText;
- mAltCode = KeySpecParser.toUpperCaseOfCodeForLocale(
+ final int altCode = KeySpecParser.toUpperCaseOfCodeForLocale(
KeySpecParser.parseCode(style.getString(keyAttr,
R.styleable.Keyboard_Key_altCode), params.mCodesSet, CODE_UNSPECIFIED),
needsToUpperCase, locale);
- mHashCode = computeHashCode(this);
-
+ 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);
+ }
+ mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
keyAttr.recycle();
-
+ mHashCode = computeHashCode(this);
if (hasShiftedLetterHint() && TextUtils.isEmpty(mHintLabel)) {
Log.w(TAG, "hasShiftedLetterHint specified without keyHintLabel: " + this);
}
}
- private static boolean needsToUpperCase(int labelFlags, int keyboardElementId) {
+ private static boolean needsToUpperCase(final int labelFlags, final int keyboardElementId) {
if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false;
switch (keyboardElementId) {
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
@@ -358,7 +383,7 @@ public class Key {
}
}
- private static int computeHashCode(Key key) {
+ private static int computeHashCode(final Key key) {
return Arrays.hashCode(new Object[] {
key.mX,
key.mY,
@@ -370,22 +395,22 @@ public class Key {
key.mIconId,
key.mBackgroundType,
Arrays.hashCode(key.mMoreKeys),
- key.mOutputText,
+ key.getOutputText(),
key.mActionFlags,
key.mLabelFlags,
// Key can be distinguishable without the following members.
- // key.mAltCode,
- // key.mDisabledIconId,
- // key.mPreviewIconId,
+ // key.mOptionalAttributes.mAltCode,
+ // key.mOptionalAttributes.mDisabledIconId,
+ // key.mOptionalAttributes.mPreviewIconId,
// key.mHorizontalGap,
// key.mVerticalGap,
- // key.mVisualInsetLeft,
- // key.mVisualInsetRight,
+ // key.mOptionalAttributes.mVisualInsetLeft,
+ // key.mOptionalAttributes.mVisualInsetRight,
// key.mMaxMoreKeysColumn,
});
}
- private boolean equals(Key o) {
+ private boolean equalsInternal(final Key o) {
if (this == o) return true;
return o.mX == mX
&& o.mY == mY
@@ -397,29 +422,42 @@ public class Key {
&& o.mIconId == mIconId
&& o.mBackgroundType == mBackgroundType
&& Arrays.equals(o.mMoreKeys, mMoreKeys)
- && TextUtils.equals(o.mOutputText, mOutputText)
+ && TextUtils.equals(o.getOutputText(), getOutputText())
&& o.mActionFlags == mActionFlags
&& o.mLabelFlags == mLabelFlags;
}
@Override
+ public int compareTo(Key o) {
+ if (equalsInternal(o)) return 0;
+ if (mHashCode > o.mHashCode) return 1;
+ return -1;
+ }
+
+ @Override
public int hashCode() {
return mHashCode;
}
@Override
- public boolean equals(Object o) {
- return o instanceof Key && equals((Key)o);
+ public boolean equals(final Object o) {
+ return o instanceof Key && equalsInternal((Key)o);
}
@Override
public String toString() {
- return String.format("%s/%s %d,%d %dx%d %s/%s/%s",
- Keyboard.printableCode(mCode), mLabel, mX, mY, mWidth, mHeight, mHintLabel,
+ final String label;
+ if (StringUtils.codePointCount(mLabel) == 1 && mLabel.codePointAt(0) == mCode) {
+ label = "";
+ } else {
+ label = "/" + mLabel;
+ }
+ return String.format("%s%s %d,%d %dx%d %s/%s/%s",
+ Keyboard.printableCode(mCode), label, mX, mY, mWidth, mHeight, mHintLabel,
KeyboardIconsSet.getIconName(mIconId), backgroundName(mBackgroundType));
}
- private static String backgroundName(int backgroundType) {
+ private static String backgroundName(final int backgroundType) {
switch (backgroundType) {
case BACKGROUND_TYPE_NORMAL: return "normal";
case BACKGROUND_TYPE_FUNCTIONAL: return "functional";
@@ -430,19 +468,19 @@ public class Key {
}
}
- public void markAsLeftEdge(Keyboard.Params params) {
+ public void markAsLeftEdge(final KeyboardParams params) {
mHitBox.left = params.mHorizontalEdgesPadding;
}
- public void markAsRightEdge(Keyboard.Params params) {
+ public void markAsRightEdge(final KeyboardParams params) {
mHitBox.right = params.mOccupiedWidth - params.mHorizontalEdgesPadding;
}
- public void markAsTopEdge(Keyboard.Params params) {
+ public void markAsTopEdge(final KeyboardParams params) {
mHitBox.top = params.mTopPadding;
}
- public void markAsBottomEdge(Keyboard.Params params) {
+ public void markAsBottomEdge(final KeyboardParams params) {
mHitBox.bottom = params.mOccupiedHeight + params.mBottomPadding;
}
@@ -450,129 +488,169 @@ public class Key {
return this instanceof Spacer;
}
- public boolean isShift() {
+ public final boolean isShift() {
return mCode == CODE_SHIFT;
}
- public boolean isModifier() {
+ public final boolean isModifier() {
return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL;
}
- public boolean isRepeatable() {
+ public final boolean isRepeatable() {
return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0;
}
- public boolean noKeyPreview() {
+ public final boolean noKeyPreview() {
return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0;
}
- public boolean altCodeWhileTyping() {
+ public final boolean altCodeWhileTyping() {
return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0;
}
- public boolean isLongPressEnabled() {
+ public final boolean isLongPressEnabled() {
// We need not start long press timer on the key which has activated shifted letter.
return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0
&& (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0;
}
- public Typeface selectTypeface(Typeface defaultTypeface) {
+ public final Typeface selectTypeface(final KeyDrawParams params) {
// TODO: Handle "bold" here too?
if ((mLabelFlags & LABEL_FLAGS_FONT_NORMAL) != 0) {
return Typeface.DEFAULT;
} else if ((mLabelFlags & LABEL_FLAGS_FONT_MONO_SPACE) != 0) {
return Typeface.MONOSPACE;
} else {
- return defaultTypeface;
+ return params.mTypeface;
}
}
- public int selectTextSize(int letterSize, int largeLetterSize, int labelSize,
- int largeLabelSize, int hintLabelSize) {
+ public final int selectTextSize(final KeyDrawParams params) {
switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) {
case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO:
- return letterSize;
+ return params.mLetterSize;
case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO:
- return largeLetterSize;
+ return params.mLargeLetterSize;
case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO:
- return labelSize;
+ return params.mLabelSize;
case LABEL_FLAGS_FOLLOW_KEY_LARGE_LABEL_RATIO:
- return largeLabelSize;
+ return params.mLargeLabelSize;
case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO:
- return hintLabelSize;
+ return params.mHintLabelSize;
default: // No follow key ratio flag specified.
- return StringUtils.codePointCount(mLabel) == 1 ? letterSize : labelSize;
+ return StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize;
+ }
+ }
+
+ public final int selectTextColor(final KeyDrawParams params) {
+ return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor;
+ }
+
+ public final int selectHintTextSize(final KeyDrawParams params) {
+ if (hasHintLabel()) {
+ return params.mHintLabelSize;
+ } else if (hasShiftedLetterHint()) {
+ return params.mShiftedLetterHintSize;
+ } else {
+ return params.mHintLetterSize;
}
}
- public boolean isAlignLeft() {
+ public final int selectHintTextColor(final KeyDrawParams params) {
+ if (hasHintLabel()) {
+ return params.mHintLabelColor;
+ } else if (hasShiftedLetterHint()) {
+ return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor
+ : params.mShiftedLetterHintInactivatedColor;
+ } else {
+ return params.mHintLetterColor;
+ }
+ }
+
+ public final int selectMoreKeyTextSize(final KeyDrawParams params) {
+ return hasLabelsInMoreKeys() ? params.mLabelSize : params.mLetterSize;
+ }
+
+ public final boolean isAlignLeft() {
return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT) != 0;
}
- public boolean isAlignRight() {
+ public final boolean isAlignRight() {
return (mLabelFlags & LABEL_FLAGS_ALIGN_RIGHT) != 0;
}
- public boolean isAlignLeftOfCenter() {
+ public final boolean isAlignLeftOfCenter() {
return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT_OF_CENTER) != 0;
}
- public boolean hasPopupHint() {
+ public final boolean hasPopupHint() {
return (mLabelFlags & LABEL_FLAGS_HAS_POPUP_HINT) != 0;
}
- public boolean hasShiftedLetterHint() {
+ public final boolean hasShiftedLetterHint() {
return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0;
}
- public boolean hasHintLabel() {
+ public final boolean hasHintLabel() {
return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0;
}
- public boolean hasLabelWithIconLeft() {
+ public final boolean hasLabelWithIconLeft() {
return (mLabelFlags & LABEL_FLAGS_WITH_ICON_LEFT) != 0;
}
- public boolean hasLabelWithIconRight() {
+ public final boolean hasLabelWithIconRight() {
return (mLabelFlags & LABEL_FLAGS_WITH_ICON_RIGHT) != 0;
}
- public boolean needsXScale() {
+ public final boolean needsXScale() {
return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0;
}
- public boolean isShiftedLetterActivated() {
+ public final boolean isShiftedLetterActivated() {
return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0;
}
- public int getMoreKeysColumn() {
+ public final int getMoreKeysColumn() {
return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_MASK;
}
- public boolean isFixedColumnOrderMoreKeys() {
+ public final boolean isFixedColumnOrderMoreKeys() {
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER) != 0;
}
- public boolean hasLabelsInMoreKeys() {
+ public final boolean hasLabelsInMoreKeys() {
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0;
}
- public int getMoreKeyLabelFlags() {
+ public final int getMoreKeyLabelFlags() {
return hasLabelsInMoreKeys()
? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO
: LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO;
}
- public boolean needsDividersInMoreKeys() {
+ public final boolean needsDividersInMoreKeys() {
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0;
}
- public boolean hasEmbeddedMoreKey() {
+ public final boolean hasEmbeddedMoreKey() {
return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_EMBEDDED_MORE_KEY) != 0;
}
- public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) {
- final int iconId = mEnabled ? mIconId : mDisabledIconId;
+ public final String getOutputText() {
+ final OptionalAttributes attrs = mOptionalAttributes;
+ return (attrs != null) ? attrs.mOutputText : null;
+ }
+
+ public final int getAltCode() {
+ final OptionalAttributes attrs = mOptionalAttributes;
+ return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED;
+ }
+
+ public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
+ final OptionalAttributes attrs = mOptionalAttributes;
+ final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED;
+ final int iconId = mEnabled ? mIconId : disabledIconId;
final Drawable icon = iconSet.getIconDrawable(iconId);
if (icon != null) {
icon.setAlpha(alpha);
@@ -580,10 +658,22 @@ public class Key {
return icon;
}
- public Drawable getPreviewIcon(KeyboardIconsSet iconSet) {
- return mPreviewIconId != ICON_UNDEFINED
- ? iconSet.getIconDrawable(mPreviewIconId)
- : iconSet.getIconDrawable(mIconId);
+ public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) {
+ final OptionalAttributes attrs = mOptionalAttributes;
+ final int previewIconId = (attrs != null) ? attrs.mPreviewIconId : ICON_UNDEFINED;
+ return previewIconId != ICON_UNDEFINED
+ ? iconSet.getIconDrawable(previewIconId) : iconSet.getIconDrawable(mIconId);
+ }
+
+ public final int getDrawX() {
+ final OptionalAttributes attrs = mOptionalAttributes;
+ return (attrs == null) ? mX : mX + attrs.mVisualInsetsLeft;
+ }
+
+ public final int getDrawWidth() {
+ final OptionalAttributes attrs = mOptionalAttributes;
+ return (attrs == null) ? mWidth
+ : mWidth - attrs.mVisualInsetsLeft - attrs.mVisualInsetsRight;
}
/**
@@ -604,11 +694,11 @@ public class Key {
mPressed = false;
}
- public boolean isEnabled() {
+ public final boolean isEnabled() {
return mEnabled;
}
- public void setEnabled(boolean enabled) {
+ public void setEnabled(final boolean enabled) {
mEnabled = enabled;
}
@@ -618,9 +708,9 @@ public class Key {
* @param y the y-coordinate of the point
* @return whether or not the point falls on the key. If the key is attached to an edge, it
* will assume that all points between the key and the edge are considered to be on the key.
- * @see #markAsLeftEdge(Keyboard.Params) etc.
+ * @see #markAsLeftEdge(KeyboardParams) etc.
*/
- public boolean isOnKey(int x, int y) {
+ public boolean isOnKey(final int x, final int y) {
return mHitBox.contains(x, y);
}
@@ -630,7 +720,7 @@ public class Key {
* @param y the y-coordinate of the point
* @return the square of the distance of the point from the nearest edge of the key
*/
- public int squaredDistanceToEdge(int x, int y) {
+ public int squaredDistanceToEdge(final int x, final int y) {
final int left = mX;
final int right = left + mWidth;
final int top = mY;
@@ -696,7 +786,7 @@ public class Key {
* @return the drawable state of the key.
* @see android.graphics.drawable.StateListDrawable#setState(int[])
*/
- public int[] getCurrentDrawableState() {
+ public final int[] getCurrentDrawableState() {
switch (mBackgroundType) {
case BACKGROUND_TYPE_FUNCTIONAL:
return mPressed ? KEY_STATE_FUNCTIONAL_PRESSED : KEY_STATE_FUNCTIONAL_NORMAL;
@@ -712,15 +802,16 @@ public class Key {
}
public static class Spacer extends Key {
- public Spacer(Resources res, Keyboard.Params params, Keyboard.Builder.Row row,
- XmlPullParser parser) throws XmlPullParserException {
+ public Spacer(final Resources res, final KeyboardParams params, final KeyboardRow row,
+ final XmlPullParser parser) throws XmlPullParserException {
super(res, params, row, parser);
}
/**
* This constructor is being used only for divider in more keys keyboard.
*/
- protected Spacer(Keyboard.Params params, int x, int y, int width, int height) {
+ 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);
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
index 13e909c7e..aa683c1d7 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
@@ -16,61 +16,71 @@
package com.android.inputmethod.keyboard;
+import com.android.inputmethod.latin.Constants;
-public class KeyDetector {
- public static final int NOT_A_CODE = -1;
+public class KeyDetector {
private final int mKeyHysteresisDistanceSquared;
+ private final int mKeyHysteresisDistanceForSlidingModifierSquared;
private Keyboard mKeyboard;
private int mCorrectionX;
private int mCorrectionY;
- private boolean mProximityCorrectOn;
/**
* This class handles key detection.
*
* @param keyHysteresisDistance if the pointer movement distance is smaller than this, the
- * movement will not been handled as meaningful movement. The unit is pixel.
+ * movement will not be handled as meaningful movement. The unit is pixel.
*/
public KeyDetector(float keyHysteresisDistance) {
+ this(keyHysteresisDistance, keyHysteresisDistance);
+ }
+
+ /**
+ * This class handles key detection.
+ *
+ * @param keyHysteresisDistance if the pointer movement distance is smaller than this, the
+ * movement will not be handled as meaningful movement. The unit is pixel.
+ * @param keyHysteresisDistanceForSlidingModifier the same parameter for sliding input that
+ * starts from a modifier key such as shift and symbols key.
+ */
+ public KeyDetector(float keyHysteresisDistance, float keyHysteresisDistanceForSlidingModifier) {
mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance);
+ mKeyHysteresisDistanceForSlidingModifierSquared = (int)(
+ keyHysteresisDistanceForSlidingModifier * keyHysteresisDistanceForSlidingModifier);
}
public void setKeyboard(Keyboard keyboard, float correctionX, float correctionY) {
- if (keyboard == null)
+ if (keyboard == null) {
throw new NullPointerException();
+ }
mCorrectionX = (int)correctionX;
mCorrectionY = (int)correctionY;
mKeyboard = keyboard;
}
- public int getKeyHysteresisDistanceSquared() {
- return mKeyHysteresisDistanceSquared;
+ public int getKeyHysteresisDistanceSquared(boolean isSlidingFromModifier) {
+ return isSlidingFromModifier
+ ? mKeyHysteresisDistanceForSlidingModifierSquared : mKeyHysteresisDistanceSquared;
}
public int getTouchX(int x) {
return x + mCorrectionX;
}
+ // TODO: Remove vertical correction.
public int getTouchY(int y) {
return y + mCorrectionY;
}
public Keyboard getKeyboard() {
- if (mKeyboard == null)
+ if (mKeyboard == null) {
throw new IllegalStateException("keyboard isn't set");
+ }
return mKeyboard;
}
- public void setProximityCorrectionEnabled(boolean enabled) {
- mProximityCorrectOn = enabled;
- }
-
- public boolean isProximityCorrectionEnabled() {
- return mProximityCorrectOn;
- }
-
public boolean alwaysAllowsSlidingInput() {
return false;
}
@@ -89,11 +99,17 @@ public class KeyDetector {
int minDistance = Integer.MAX_VALUE;
Key primaryKey = null;
for (final Key key: mKeyboard.getNearestKeys(touchX, touchY)) {
- final boolean isOnKey = key.isOnKey(touchX, touchY);
+ // An edge key always has its enlarged hitbox to respond to an event that occurred in
+ // the empty area around the key. (@see Key#markAsLeftEdge(KeyboardParams)} etc.)
+ if (!key.isOnKey(touchX, touchY)) {
+ continue;
+ }
final int distance = key.squaredDistanceToEdge(touchX, touchY);
+ if (distance > minDistance) {
+ continue;
+ }
// To take care of hitbox overlaps, we compare mCode here too.
- if (primaryKey == null || distance < minDistance
- || (distance == minDistance && isOnKey && key.mCode > primaryKey.mCode)) {
+ if (primaryKey == null || distance < minDistance || key.mCode > primaryKey.mCode) {
minDistance = distance;
primaryKey = key;
}
@@ -109,7 +125,7 @@ public class KeyDetector {
final StringBuilder sb = new StringBuilder();
boolean addDelimiter = false;
for (final int code : codes) {
- if (code == NOT_A_CODE) break;
+ if (code == Constants.NOT_A_CODE) break;
if (addDelimiter) sb.append(", ");
sb.append(Keyboard.printableCode(code));
addDelimiter = true;
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index 21f175d7d..b7c7f415d 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -16,37 +16,15 @@
package com.android.inputmethod.keyboard;
-import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.content.res.XmlResourceParser;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
import android.util.Log;
-import android.util.TypedValue;
-import android.util.Xml;
-import android.view.InflateException;
+import android.util.SparseArray;
-import com.android.inputmethod.keyboard.internal.KeyStyles;
-import com.android.inputmethod.keyboard.internal.KeyboardCodesSet;
+import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
-import com.android.inputmethod.keyboard.internal.KeyboardTextsSet;
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.SubtypeLocale;
-import com.android.inputmethod.latin.Utils;
-import com.android.inputmethod.latin.XmlParseUtils;
+import com.android.inputmethod.keyboard.internal.KeyboardParams;
+import com.android.inputmethod.latin.CollectionUtils;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Locale;
/**
* Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
@@ -79,6 +57,8 @@ public class Keyboard {
public static final int CODE_DASH = '-';
public static final int CODE_SINGLE_QUOTE = '\'';
public static final int CODE_DOUBLE_QUOTE = '"';
+ public static final int CODE_QUESTION_MARK = '?';
+ public static final int CODE_EXCLAMATION_MARK = '!';
// 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?
@@ -86,10 +66,10 @@ public class Keyboard {
public static final int CODE_CLOSING_SQUARE_BRACKET = ']';
public static final int CODE_CLOSING_CURLY_BRACKET = '}';
public static final int CODE_CLOSING_ANGLE_BRACKET = '>';
- private static final int MINIMUM_LETTER_CODE = CODE_TAB;
/** Special keys code. Must be negative.
- * These should be aligned with values/keycodes.xml
+ * These should be aligned with KeyboardCodesSet.ID_TO_NAME[],
+ * KeyboardCodesSet.DEFAULT[] and KeyboardCodesSet.RTL[]
*/
public static final int CODE_SHIFT = -1;
public static final int CODE_SWITCH_ALPHA_SYMBOL = -2;
@@ -101,8 +81,9 @@ public class Keyboard {
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;
// Code value representing the code is not specified.
- public static final int CODE_UNSPECIFIED = -11;
+ public static final int CODE_UNSPECIFIED = -12;
public final KeyboardId mId;
public final int mThemeId;
@@ -117,6 +98,9 @@ public class Keyboard {
/** Default gap between rows */
public final int mVerticalGap;
+ /** Per keyboard key visual parameters */
+ public final KeyVisualAttributes mKeyVisualAttributes;
+
public final int mMostCommonKeyHeight;
public final int mMostCommonKeyWidth;
@@ -132,12 +116,12 @@ public class Keyboard {
public final Key[] mAltCodeKeysWhileTyping;
public final KeyboardIconsSet mIconsSet;
- private final HashMap<Integer, Key> mKeyCache = new HashMap<Integer, Key>();
+ private final SparseArray<Key> mKeyCache = CollectionUtils.newSparseArray();
private final ProximityInfo mProximityInfo;
private final boolean mProximityCharsCorrectionEnabled;
- public Keyboard(Params params) {
+ public Keyboard(final KeyboardParams params) {
mId = params.mId;
mThemeId = params.mThemeId;
mOccupiedHeight = params.mOccupiedHeight;
@@ -146,7 +130,7 @@ public class Keyboard {
mMostCommonKeyWidth = params.mMostCommonKeyWidth;
mMoreKeysTemplate = params.mMoreKeysTemplate;
mMaxMoreKeysKeyboardColumn = params.mMaxMoreKeysKeyboardColumn;
-
+ mKeyVisualAttributes = params.mKeyVisualAttributes;
mTopPadding = params.mTopPadding;
mVerticalGap = params.mVerticalGap;
@@ -162,7 +146,7 @@ public class Keyboard {
mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled;
}
- public boolean hasProximityCharsCorrection(int code) {
+ public boolean hasProximityCharsCorrection(final int code) {
if (!mProximityCharsCorrectionEnabled) {
return false;
}
@@ -178,27 +162,29 @@ public class Keyboard {
return mProximityInfo;
}
- public Key getKey(int code) {
+ public Key getKey(final int code) {
if (code == CODE_UNSPECIFIED) {
return null;
}
- final Integer keyCode = code;
- if (mKeyCache.containsKey(keyCode)) {
- return mKeyCache.get(keyCode);
- }
+ synchronized (mKeyCache) {
+ final int index = mKeyCache.indexOfKey(code);
+ if (index >= 0) {
+ return mKeyCache.valueAt(index);
+ }
- for (final Key key : mKeys) {
- if (key.mCode == code) {
- mKeyCache.put(keyCode, key);
- return key;
+ for (final Key key : mKeys) {
+ if (key.mCode == code) {
+ mKeyCache.put(code, key);
+ return key;
+ }
}
+ mKeyCache.put(code, null);
+ return null;
}
- mKeyCache.put(keyCode, null);
- return null;
}
- public boolean hasKey(Key aKey) {
- if (mKeyCache.containsKey(aKey)) {
+ public boolean hasKey(final Key aKey) {
+ if (mKeyCache.indexOfValue(aKey) >= 0) {
return true;
}
@@ -211,172 +197,13 @@ public class Keyboard {
return false;
}
- public static boolean isLetterCode(int code) {
- return code >= MINIMUM_LETTER_CODE;
+ public static boolean isLetterCode(final int code) {
+ return code >= CODE_SPACE;
}
- public static class Params {
- public KeyboardId mId;
- public int mThemeId;
-
- /** Total height and width of the keyboard, including the paddings and keys */
- public int mOccupiedHeight;
- public int mOccupiedWidth;
-
- /** Base height and width of the keyboard used to calculate rows' or keys' heights and
- * widths
- */
- public int mBaseHeight;
- public int mBaseWidth;
-
- public int mTopPadding;
- public int mBottomPadding;
- public int mHorizontalEdgesPadding;
- public int mHorizontalCenterPadding;
-
- public int mDefaultRowHeight;
- public int mDefaultKeyWidth;
- public int mHorizontalGap;
- public int mVerticalGap;
-
- public int mMoreKeysTemplate;
- public int mMaxMoreKeysKeyboardColumn;
-
- public int GRID_WIDTH;
- public int GRID_HEIGHT;
-
- public final HashSet<Key> mKeys = new HashSet<Key>();
- public final ArrayList<Key> mShiftKeys = new ArrayList<Key>();
- public final ArrayList<Key> mAltCodeKeysWhileTyping = new ArrayList<Key>();
- public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
- public final KeyboardCodesSet mCodesSet = new KeyboardCodesSet();
- public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
- public final KeyStyles mKeyStyles = new KeyStyles(mTextsSet);
-
- public KeyboardLayoutSet.KeysCache mKeysCache;
-
- public int mMostCommonKeyHeight = 0;
- public int mMostCommonKeyWidth = 0;
-
- public boolean mProximityCharsCorrectionEnabled;
-
- public final TouchPositionCorrection mTouchPositionCorrection =
- new TouchPositionCorrection();
-
- public static class TouchPositionCorrection {
- private static final int TOUCH_POSITION_CORRECTION_RECORD_SIZE = 3;
-
- public boolean mEnabled;
- public float[] mXs;
- public float[] mYs;
- public float[] mRadii;
-
- public void load(String[] data) {
- final int dataLength = data.length;
- if (dataLength % TOUCH_POSITION_CORRECTION_RECORD_SIZE != 0) {
- if (LatinImeLogger.sDBG)
- throw new RuntimeException(
- "the size of touch position correction data is invalid");
- return;
- }
-
- final int length = dataLength / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
- mXs = new float[length];
- mYs = new float[length];
- mRadii = new float[length];
- try {
- for (int i = 0; i < dataLength; ++i) {
- final int type = i % TOUCH_POSITION_CORRECTION_RECORD_SIZE;
- final int index = i / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
- final float value = Float.parseFloat(data[i]);
- if (type == 0) {
- mXs[index] = value;
- } else if (type == 1) {
- mYs[index] = value;
- } else {
- mRadii[index] = value;
- }
- }
- } catch (NumberFormatException e) {
- if (LatinImeLogger.sDBG) {
- throw new RuntimeException(
- "the number format for touch position correction data is invalid");
- }
- mXs = null;
- mYs = null;
- mRadii = null;
- }
- }
-
- // TODO: Remove this method.
- public void setEnabled(boolean enabled) {
- mEnabled = enabled;
- }
-
- public boolean isValid() {
- return mEnabled && mXs != null && mYs != null && mRadii != null
- && mXs.length > 0 && mYs.length > 0 && mRadii.length > 0;
- }
- }
-
- protected void clearKeys() {
- mKeys.clear();
- mShiftKeys.clear();
- clearHistogram();
- }
-
- public void onAddKey(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);
- }
- if (key.mCode == Keyboard.CODE_SHIFT) {
- mShiftKeys.add(key);
- }
- if (key.altCodeWhileTyping()) {
- mAltCodeKeysWhileTyping.add(key);
- }
- }
-
- private int mMaxHeightCount = 0;
- private int mMaxWidthCount = 0;
- private final HashMap<Integer, Integer> mHeightHistogram = new HashMap<Integer, Integer>();
- private final HashMap<Integer, Integer> mWidthHistogram = new HashMap<Integer, Integer>();
-
- private void clearHistogram() {
- mMostCommonKeyHeight = 0;
- mMaxHeightCount = 0;
- mHeightHistogram.clear();
-
- mMaxWidthCount = 0;
- mMostCommonKeyWidth = 0;
- mWidthHistogram.clear();
- }
-
- private static int updateHistogramCounter(HashMap<Integer, Integer> histogram,
- Integer key) {
- final int count = (histogram.containsKey(key) ? histogram.get(key) : 0) + 1;
- histogram.put(key, count);
- return count;
- }
-
- private void updateHistogram(Key key) {
- final Integer height = key.mHeight + key.mVerticalGap;
- final int heightCount = updateHistogramCounter(mHeightHistogram, height);
- if (heightCount > mMaxHeightCount) {
- mMaxHeightCount = heightCount;
- mMostCommonKeyHeight = height;
- }
-
- final Integer width = key.mWidth + key.mHorizontalGap;
- final int widthCount = updateHistogramCounter(mWidthHistogram, width);
- if (widthCount > mMaxWidthCount) {
- mMaxWidthCount = widthCount;
- mMostCommonKeyWidth = width;
- }
- }
+ @Override
+ public String toString() {
+ return mId.toString();
}
/**
@@ -386,14 +213,14 @@ public class Keyboard {
* @return the array of the nearest keys to the given point. If the given
* point is out of range, then an array of size zero is returned.
*/
- public Key[] getNearestKeys(int x, int y) {
+ public Key[] getNearestKeys(final int x, final int y) {
// Avoid dead pixels at edges of the keyboard
final int adjustedX = Math.max(0, Math.min(x, mOccupiedWidth - 1));
final int adjustedY = Math.max(0, Math.min(y, mOccupiedHeight - 1));
return mProximityInfo.getNearestKeys(adjustedX, adjustedY);
}
- public static String printableCode(int code) {
+ public static String printableCode(final int code) {
switch (code) {
case CODE_SHIFT: return "shift";
case CODE_SWITCH_ALPHA_SYMBOL: return "symbol";
@@ -415,937 +242,4 @@ public class Keyboard {
return String.format("'\\u%04x'", code);
}
}
-
- /**
- * Keyboard Building helper.
- *
- * This class parses Keyboard XML file and eventually build a Keyboard.
- * The Keyboard XML file looks like:
- * <pre>
- * &gt;!-- xml/keyboard.xml --&lt;
- * &gt;Keyboard keyboard_attributes*&lt;
- * &gt;!-- Keyboard Content --&lt;
- * &gt;Row row_attributes*&lt;
- * &gt;!-- Row Content --&lt;
- * &gt;Key key_attributes* /&lt;
- * &gt;Spacer horizontalGap="32.0dp" /&lt;
- * &gt;include keyboardLayout="@xml/other_keys"&lt;
- * ...
- * &gt;/Row&lt;
- * &gt;include keyboardLayout="@xml/other_rows"&lt;
- * ...
- * &gt;/Keyboard&lt;
- * </pre>
- * The XML file which is included in other file must have &gt;merge&lt; as root element,
- * such as:
- * <pre>
- * &gt;!-- xml/other_keys.xml --&lt;
- * &gt;merge&lt;
- * &gt;Key key_attributes* /&lt;
- * ...
- * &gt;/merge&lt;
- * </pre>
- * and
- * <pre>
- * &gt;!-- xml/other_rows.xml --&lt;
- * &gt;merge&lt;
- * &gt;Row row_attributes*&lt;
- * &gt;Key key_attributes* /&lt;
- * &gt;/Row&lt;
- * ...
- * &gt;/merge&lt;
- * </pre>
- * You can also use switch-case-default tags to select Rows and Keys.
- * <pre>
- * &gt;switch&lt;
- * &gt;case case_attribute*&lt;
- * &gt;!-- Any valid tags at switch position --&lt;
- * &gt;/case&lt;
- * ...
- * &gt;default&lt;
- * &gt;!-- Any valid tags at switch position --&lt;
- * &gt;/default&lt;
- * &gt;/switch&lt;
- * </pre>
- * You can declare Key style and specify styles within Key tags.
- * <pre>
- * &gt;switch&lt;
- * &gt;case mode="email"&lt;
- * &gt;key-style styleName="f1-key" parentStyle="modifier-key"
- * keyLabel=".com"
- * /&lt;
- * &gt;/case&lt;
- * &gt;case mode="url"&lt;
- * &gt;key-style styleName="f1-key" parentStyle="modifier-key"
- * keyLabel="http://"
- * /&lt;
- * &gt;/case&lt;
- * &gt;/switch&lt;
- * ...
- * &gt;Key keyStyle="shift-key" ... /&lt;
- * </pre>
- */
-
- public static class Builder<KP extends Params> {
- private static final String BUILDER_TAG = "Keyboard.Builder";
- private static final boolean DEBUG = false;
-
- // Keyboard XML Tags
- private static final String TAG_KEYBOARD = "Keyboard";
- private static final String TAG_ROW = "Row";
- private static final String TAG_KEY = "Key";
- private static final String TAG_SPACER = "Spacer";
- private static final String TAG_INCLUDE = "include";
- private static final String TAG_MERGE = "merge";
- private static final String TAG_SWITCH = "switch";
- private static final String TAG_CASE = "case";
- private static final String TAG_DEFAULT = "default";
- public static final String TAG_KEY_STYLE = "key-style";
-
- private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
- private static final int DEFAULT_KEYBOARD_ROWS = 4;
-
- protected final KP mParams;
- protected final Context mContext;
- protected final Resources mResources;
- private final DisplayMetrics mDisplayMetrics;
-
- private int mCurrentY = 0;
- private Row mCurrentRow = null;
- private boolean mLeftEdge;
- private boolean mTopEdge;
- private Key mRightEdgeKey = null;
-
- /**
- * 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}
- * defines.
- */
- public static class Row {
- // keyWidth enum constants
- private static final int KEYWIDTH_NOT_ENUM = 0;
- private static final int KEYWIDTH_FILL_RIGHT = -1;
-
- private final Params 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;
-
- private final int mCurrentY;
- // Will be updated by {@link Key}'s constructor.
- private float mCurrentX;
-
- public Row(Resources res, Params params, XmlPullParser parser, int y) {
- mParams = params;
- TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard);
- mRowHeight = (int)Builder.getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_rowHeight,
- params.mBaseHeight, params.mDefaultRowHeight);
- keyboardAttr.recycle();
- TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Key);
- mDefaultKeyWidth = Builder.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_keyWidth,
- params.mBaseWidth, params.mDefaultKeyWidth);
- mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
- Key.BACKGROUND_TYPE_NORMAL);
- 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 void setDefaultKeyWidth(float defaultKeyWidth) {
- mDefaultKeyWidth = defaultKeyWidth;
- }
-
- public int getDefaultKeyLabelFlags() {
- return mDefaultKeyLabelFlags;
- }
-
- public void setDefaultKeyLabelFlags(int keyLabelFlags) {
- mDefaultKeyLabelFlags = keyLabelFlags;
- }
-
- public int getDefaultBackgroundType() {
- return mDefaultBackgroundType;
- }
-
- public void setDefaultBackgroundType(int backgroundType) {
- mDefaultBackgroundType = backgroundType;
- }
-
- public void setXPos(float keyXPos) {
- mCurrentX = keyXPos;
- }
-
- public void advanceXPos(float width) {
- mCurrentX += width;
- }
-
- public int getKeyY() {
- return mCurrentY;
- }
-
- public float getKeyX(TypedArray keyAttr) {
- final int widthType = Builder.getEnumValue(keyAttr,
- R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
-
- final int keyboardRightEdge = mParams.mOccupiedWidth
- - mParams.mHorizontalEdgesPadding;
- if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
- final float keyXPos = Builder.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_keyXPos, 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.
- return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
- } else {
- return keyXPos + mParams.mHorizontalEdgesPadding;
- }
- }
- return mCurrentX;
- }
-
- public float getKeyWidth(TypedArray keyAttr) {
- return getKeyWidth(keyAttr, mCurrentX);
- }
-
- public float getKeyWidth(TypedArray keyAttr, float keyXPos) {
- final int widthType = Builder.getEnumValue(keyAttr,
- R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
- switch (widthType) {
- case KEYWIDTH_FILL_RIGHT:
- final int keyboardRightEdge =
- mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
- // If keyWidth is fillRight, the actual key width will be determined to fill
- // out the area up to the right edge of the keyboard.
- return keyboardRightEdge - keyXPos;
- default: // KEYWIDTH_NOT_ENUM
- return Builder.getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_keyWidth,
- mParams.mBaseWidth, mDefaultKeyWidth);
- }
- }
- }
-
- public Builder(Context context, KP params) {
- mContext = context;
- final Resources res = context.getResources();
- mResources = res;
- mDisplayMetrics = res.getDisplayMetrics();
-
- mParams = params;
-
- params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
- params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
- }
-
- public void setAutoGenerate(KeyboardLayoutSet.KeysCache keysCache) {
- mParams.mKeysCache = keysCache;
- }
-
- public Builder<KP> load(int xmlId, KeyboardId id) {
- mParams.mId = id;
- final XmlResourceParser parser = mResources.getXml(xmlId);
- try {
- parseKeyboard(parser);
- } catch (XmlPullParserException e) {
- Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
- throw new IllegalArgumentException(e);
- } catch (IOException e) {
- Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
- throw new RuntimeException(e);
- } finally {
- parser.close();
- }
- return this;
- }
-
- // TODO: Remove this method.
- public void setTouchPositionCorrectionEnabled(boolean enabled) {
- mParams.mTouchPositionCorrection.setEnabled(enabled);
- }
-
- public void setProximityCharsCorrectionEnabled(boolean enabled) {
- mParams.mProximityCharsCorrectionEnabled = enabled;
- }
-
- public Keyboard build() {
- return new Keyboard(mParams);
- }
-
- private int mIndent;
- private static final String SPACES = " ";
-
- private static String spaces(int count) {
- return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
- }
-
- private void startTag(String format, Object ... args) {
- Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
- }
-
- private void endTag(String format, Object ... args) {
- Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
- }
-
- private void startEndTag(String format, Object ... args) {
- Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
- mIndent--;
- }
-
- private void parseKeyboard(XmlPullParser parser)
- throws XmlPullParserException, IOException {
- if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId);
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (event == XmlPullParser.START_TAG) {
- final String tag = parser.getName();
- if (TAG_KEYBOARD.equals(tag)) {
- parseKeyboardAttributes(parser);
- startKeyboard();
- parseKeyboardContent(parser, false);
- break;
- } else {
- throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD);
- }
- }
- }
- }
-
- private void parseKeyboardAttributes(XmlPullParser parser) {
- final int displayWidth = mDisplayMetrics.widthPixels;
- 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);
- try {
- final int displayHeight = mDisplayMetrics.heightPixels;
- final String keyboardHeightString = Utils.getDeviceOverrideValue(
- mResources, R.array.keyboard_heights, null);
- final float keyboardHeight;
- if (keyboardHeightString != null) {
- keyboardHeight = Float.parseFloat(keyboardHeightString)
- * mDisplayMetrics.density;
- } else {
- keyboardHeight = keyboardAttr.getDimension(
- R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
- }
- final float maxKeyboardHeight = getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
- float minKeyboardHeight = getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
- if (minKeyboardHeight < 0) {
- // Specified fraction was negative, so it should be calculated against display
- // width.
- minKeyboardHeight = -getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
- }
- final Params params = mParams;
- // Keyboard height will not exceed maxKeyboardHeight and will not be less than
- // minKeyboardHeight.
- params.mOccupiedHeight = (int)Math.max(
- Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
- params.mOccupiedWidth = params.mId.mWidth;
- params.mTopPadding = (int)getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
- params.mBottomPadding = (int)getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
- params.mHorizontalEdgesPadding = (int)getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_keyboardHorizontalEdgesPadding,
- mParams.mOccupiedWidth, 0);
-
- params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
- - params.mHorizontalCenterPadding;
- params.mDefaultKeyWidth = (int)getDimensionOrFraction(keyAttr,
- R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
- params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
- params.mHorizontalGap = (int)getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
- params.mVerticalGap = (int)getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
- params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
- - params.mBottomPadding + params.mVerticalGap;
- params.mDefaultRowHeight = (int)getDimensionOrFraction(keyboardAttr,
- R.styleable.Keyboard_rowHeight, params.mBaseHeight,
- params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
-
- params.mMoreKeysTemplate = keyboardAttr.getResourceId(
- R.styleable.Keyboard_moreKeysTemplate, 0);
- params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
- R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
-
- params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0);
- params.mIconsSet.loadIcons(keyboardAttr);
- final String language = params.mId.mLocale.getLanguage();
- params.mCodesSet.setLanguage(language);
- params.mTextsSet.setLanguage(language);
- final RunInLocale<Void> job = new RunInLocale<Void>() {
- @Override
- protected Void job(Resources res) {
- params.mTextsSet.loadStringResources(mContext);
- return null;
- }
- };
- // Null means the current system locale.
- final Locale locale = SubtypeLocale.isNoLanguage(params.mId.mSubtype)
- ? null : params.mId.mLocale;
- job.runInLocale(mResources, locale);
-
- final int resourceId = keyboardAttr.getResourceId(
- R.styleable.Keyboard_touchPositionCorrectionData, 0);
- params.mTouchPositionCorrection.setEnabled(resourceId != 0);
- if (resourceId != 0) {
- final String[] data = mResources.getStringArray(resourceId);
- params.mTouchPositionCorrection.load(data);
- }
- } finally {
- keyAttr.recycle();
- keyboardAttr.recycle();
- }
- }
-
- private void parseKeyboardContent(XmlPullParser parser, boolean skip)
- throws XmlPullParserException, IOException {
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (event == XmlPullParser.START_TAG) {
- final String tag = parser.getName();
- if (TAG_ROW.equals(tag)) {
- Row row = parseRowAttributes(parser);
- if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
- if (!skip) {
- startRow(row);
- }
- parseRowContent(parser, row, skip);
- } else if (TAG_INCLUDE.equals(tag)) {
- parseIncludeKeyboardContent(parser, skip);
- } else if (TAG_SWITCH.equals(tag)) {
- parseSwitchKeyboardContent(parser, skip);
- } else if (TAG_KEY_STYLE.equals(tag)) {
- parseKeyStyle(parser, skip);
- } else {
- throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW);
- }
- } else if (event == XmlPullParser.END_TAG) {
- final String tag = parser.getName();
- 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_ROW);
- }
- }
- }
- }
-
- private Row parseRowAttributes(XmlPullParser parser) throws XmlPullParserException {
- final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard);
- try {
- if (a.hasValue(R.styleable.Keyboard_horizontalGap))
- throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap");
- if (a.hasValue(R.styleable.Keyboard_verticalGap))
- throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap");
- return new Row(mResources, mParams, parser, mCurrentY);
- } finally {
- a.recycle();
- }
- }
-
- private void parseRowContent(XmlPullParser parser, Row row, boolean skip)
- throws XmlPullParserException, IOException {
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (event == XmlPullParser.START_TAG) {
- final String tag = parser.getName();
- if (TAG_KEY.equals(tag)) {
- parseKey(parser, row, skip);
- } else if (TAG_SPACER.equals(tag)) {
- parseSpacer(parser, row, skip);
- } else if (TAG_INCLUDE.equals(tag)) {
- parseIncludeRowContent(parser, row, skip);
- } else if (TAG_SWITCH.equals(tag)) {
- parseSwitchRowContent(parser, row, skip);
- } else if (TAG_KEY_STYLE.equals(tag)) {
- parseKeyStyle(parser, skip);
- } else {
- throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
- }
- } else if (event == XmlPullParser.END_TAG) {
- final String tag = parser.getName();
- if (DEBUG) endTag("</%s>", tag);
- if (TAG_ROW.equals(tag)) {
- 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_KEY);
- }
- }
- }
- }
-
- private void parseKey(XmlPullParser parser, Row row, boolean skip)
- throws XmlPullParserException, IOException {
- if (skip) {
- XmlParseUtils.checkEndTag(TAG_KEY, parser);
- if (DEBUG) startEndTag("<%s /> skipped", TAG_KEY);
- } 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));
- }
- XmlParseUtils.checkEndTag(TAG_KEY, parser);
- endKey(key);
- }
- }
-
- private void parseSpacer(XmlPullParser parser, Row row, boolean skip)
- throws XmlPullParserException, IOException {
- 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);
- }
- }
-
- private void parseIncludeKeyboardContent(XmlPullParser parser, boolean skip)
- throws XmlPullParserException, IOException {
- parseIncludeInternal(parser, null, skip);
- }
-
- private void parseIncludeRowContent(XmlPullParser parser, Row row, boolean skip)
- throws XmlPullParserException, IOException {
- parseIncludeInternal(parser, row, skip);
- }
-
- private void parseIncludeInternal(XmlPullParser parser, Row row, boolean skip)
- throws XmlPullParserException, IOException {
- 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();
- }
-
- 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();
- }
- }
- }
-
- private void parseMerge(XmlPullParser parser, Row row, boolean skip)
- throws XmlPullParserException, IOException {
- if (DEBUG) startTag("<%s>", TAG_MERGE);
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (event == XmlPullParser.START_TAG) {
- final String tag = parser.getName();
- if (TAG_MERGE.equals(tag)) {
- if (row == null) {
- parseKeyboardContent(parser, skip);
- } else {
- parseRowContent(parser, row, skip);
- }
- break;
- } else {
- throw new XmlParseUtils.ParseException(
- "Included keyboard layout must have <merge> root element", parser);
- }
- }
- }
- }
-
- private void parseSwitchKeyboardContent(XmlPullParser parser, boolean skip)
- throws XmlPullParserException, IOException {
- parseSwitchInternal(parser, null, skip);
- }
-
- private void parseSwitchRowContent(XmlPullParser parser, Row row, boolean skip)
- throws XmlPullParserException, IOException {
- parseSwitchInternal(parser, row, skip);
- }
-
- private void parseSwitchInternal(XmlPullParser parser, Row row, boolean skip)
- throws XmlPullParserException, IOException {
- if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
- boolean selected = false;
- int event;
- while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
- if (event == XmlPullParser.START_TAG) {
- final String tag = parser.getName();
- if (TAG_CASE.equals(tag)) {
- selected |= parseCase(parser, row, selected ? true : skip);
- } else if (TAG_DEFAULT.equals(tag)) {
- selected |= parseDefault(parser, row, selected ? true : skip);
- } else {
- throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
- }
- } else if (event == XmlPullParser.END_TAG) {
- final String tag = parser.getName();
- if (TAG_SWITCH.equals(tag)) {
- if (DEBUG) endTag("</%s>", TAG_SWITCH);
- break;
- } else {
- throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
- }
- }
- }
- }
-
- private boolean parseCase(XmlPullParser parser, Row row, boolean skip)
- throws XmlPullParserException, IOException {
- final boolean selected = parseCaseCondition(parser);
- if (row == null) {
- // Processing Rows.
- parseKeyboardContent(parser, selected ? skip : true);
- } else {
- // Processing Keys.
- parseRowContent(parser, row, selected ? skip : true);
- }
- return selected;
- }
-
- private boolean parseCaseCondition(XmlPullParser parser) {
- final KeyboardId id = mParams.mId;
- if (id == null)
- return true;
-
- final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
- R.styleable.Keyboard_Case);
- try {
- final boolean keyboardLayoutSetElementMatched = matchTypedValue(a,
- R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
- KeyboardId.elementIdToName(id.mElementId));
- final boolean modeMatched = matchTypedValue(a,
- R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
- final boolean navigateNextMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
- final boolean navigatePreviousMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
- final boolean passwordInputMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
- final boolean clobberSettingsKeyMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
- final boolean shortcutKeyEnabledMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
- final boolean hasShortcutKeyMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
- final boolean languageSwitchKeyEnabledMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
- id.mLanguageSwitchKeyEnabled);
- final boolean isMultiLineMatched = matchBoolean(a,
- R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
- final boolean imeActionMatched = matchInteger(a,
- R.styleable.Keyboard_Case_imeAction, id.imeAction());
- final boolean localeCodeMatched = matchString(a,
- R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
- final boolean languageCodeMatched = matchString(a,
- R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
- final boolean countryCodeMatched = matchString(a,
- R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
- final boolean selected = keyboardLayoutSetElementMatched && modeMatched
- && navigateNextMatched && navigatePreviousMatched && passwordInputMatched
- && clobberSettingsKeyMatched && shortcutKeyEnabledMatched
- && 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", TAG_CASE,
- textAttr(a.getString(
- R.styleable.Keyboard_Case_keyboardLayoutSetElement),
- "keyboardLayoutSetElement"),
- textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
- textAttr(a.getString(R.styleable.Keyboard_Case_imeAction),
- "imeAction"),
- booleanAttr(a, R.styleable.Keyboard_Case_navigateNext,
- "navigateNext"),
- booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious,
- "navigatePrevious"),
- booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
- "clobberSettingsKey"),
- booleanAttr(a, R.styleable.Keyboard_Case_passwordInput,
- "passwordInput"),
- booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled,
- "shortcutKeyEnabled"),
- booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey,
- "hasShortcutKey"),
- booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
- "languageSwitchKeyEnabled"),
- booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine,
- "isMultiLine"),
- textAttr(a.getString(R.styleable.Keyboard_Case_localeCode),
- "localeCode"),
- textAttr(a.getString(R.styleable.Keyboard_Case_languageCode),
- "languageCode"),
- textAttr(a.getString(R.styleable.Keyboard_Case_countryCode),
- "countryCode"),
- selected ? "" : " skipped");
- }
-
- return selected;
- } finally {
- a.recycle();
- }
- }
-
- private static boolean matchInteger(TypedArray a, int index, int value) {
- // If <case> does not have "index" attribute, that means this <case> is wild-card for
- // the attribute.
- return !a.hasValue(index) || a.getInt(index, 0) == value;
- }
-
- private static boolean matchBoolean(TypedArray a, int index, boolean value) {
- // If <case> does not have "index" attribute, that means this <case> is wild-card for
- // the attribute.
- return !a.hasValue(index) || a.getBoolean(index, false) == value;
- }
-
- private static boolean matchString(TypedArray a, int index, String value) {
- // If <case> does not have "index" attribute, that means this <case> is wild-card for
- // the attribute.
- return !a.hasValue(index)
- || stringArrayContains(a.getString(index).split("\\|"), value);
- }
-
- private static boolean matchTypedValue(TypedArray a, int index, int intValue,
- String strValue) {
- // If <case> does not have "index" attribute, that means this <case> is wild-card for
- // the attribute.
- final TypedValue v = a.peekValue(index);
- if (v == null)
- return true;
-
- if (isIntegerValue(v)) {
- return intValue == a.getInt(index, 0);
- } else if (isStringValue(v)) {
- return stringArrayContains(a.getString(index).split("\\|"), strValue);
- }
- return false;
- }
-
- private static boolean stringArrayContains(String[] array, String value) {
- for (final String elem : array) {
- if (elem.equals(value))
- return true;
- }
- return false;
- }
-
- private boolean parseDefault(XmlPullParser parser, Row row, boolean skip)
- throws XmlPullParserException, IOException {
- if (DEBUG) startTag("<%s>", TAG_DEFAULT);
- if (row == null) {
- parseKeyboardContent(parser, skip);
- } else {
- parseRowContent(parser, row, skip);
- }
- return true;
- }
-
- private void parseKeyStyle(XmlPullParser parser, 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);
- try {
- if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName))
- throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
- + "/> needs styleName attribute", parser);
- if (DEBUG) {
- startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
- keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
- skip ? " skipped" : "");
- }
- if (!skip)
- mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
- } finally {
- keyStyleAttr.recycle();
- keyAttrs.recycle();
- }
- XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
- }
-
- private void startKeyboard() {
- mCurrentY += mParams.mTopPadding;
- mTopEdge = true;
- }
-
- private void startRow(Row row) {
- addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
- mCurrentRow = row;
- mLeftEdge = true;
- mRightEdgeKey = null;
- }
-
- private void endRow(Row row) {
- if (mCurrentRow == null)
- throw new InflateException("orphan end row tag");
- if (mRightEdgeKey != null) {
- mRightEdgeKey.markAsRightEdge(mParams);
- mRightEdgeKey = null;
- }
- addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
- mCurrentY += row.mRowHeight;
- mCurrentRow = null;
- mTopEdge = false;
- }
-
- private void endKey(Key key) {
- mParams.onAddKey(key);
- if (mLeftEdge) {
- key.markAsLeftEdge(mParams);
- mLeftEdge = false;
- }
- if (mTopEdge) {
- key.markAsTopEdge(mParams);
- }
- mRightEdgeKey = key;
- }
-
- private void endKeyboard() {
- // nothing to do here.
- }
-
- private void addEdgeSpace(float width, Row row) {
- row.advanceXPos(width);
- mLeftEdge = false;
- mRightEdgeKey = null;
- }
-
- public static float getDimensionOrFraction(TypedArray a, int index, int base,
- float defValue) {
- final TypedValue value = a.peekValue(index);
- if (value == null)
- return defValue;
- if (isFractionValue(value)) {
- return a.getFraction(index, base, base, defValue);
- } else if (isDimensionValue(value)) {
- return a.getDimension(index, defValue);
- }
- return defValue;
- }
-
- public static int getEnumValue(TypedArray a, int index, int defValue) {
- final TypedValue value = a.peekValue(index);
- if (value == null)
- return defValue;
- if (isIntegerValue(value)) {
- return a.getInt(index, defValue);
- }
- return defValue;
- }
-
- private static boolean isFractionValue(TypedValue v) {
- return v.type == TypedValue.TYPE_FRACTION;
- }
-
- private static boolean isDimensionValue(TypedValue v) {
- return v.type == TypedValue.TYPE_DIMENSION;
- }
-
- private static boolean isIntegerValue(TypedValue v) {
- return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
- }
-
- private static boolean isStringValue(TypedValue v) {
- return v.type == TypedValue.TYPE_STRING;
- }
-
- private static String textAttr(String value, String name) {
- return value != null ? String.format(" %s=%s", name, value) : "";
- }
-
- private static String booleanAttr(TypedArray a, int index, String name) {
- return a.hasValue(index)
- ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
- }
- }
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
index 275aacf36..5c8f78f5e 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
@@ -16,6 +16,9 @@
package com.android.inputmethod.keyboard;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.InputPointers;
+
public interface KeyboardActionListener {
/**
@@ -42,20 +45,16 @@ public interface KeyboardActionListener {
*
* @param primaryCode this is the code of the key that was pressed
* @param x x-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
- * {@link PointerTracker} or so, the value should be {@link #NOT_A_TOUCH_COORDINATE}.
- * If it's called on insertion from the suggestion strip, it should be
- * {@link #SUGGESTION_STRIP_COORDINATE}.
+ * {@link PointerTracker} or so, the value should be
+ * {@link Constants#NOT_A_COORDINATE}. If it's called on insertion from the
+ * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}.
* @param y y-coordinate pixel of touched event. If {@link #onCodeInput} is not called by
- * {@link PointerTracker} or so, the value should be {@link #NOT_A_TOUCH_COORDINATE}.
- * If it's called on insertion from the suggestion strip, it should be
- * {@link #SUGGESTION_STRIP_COORDINATE}.
+ * {@link PointerTracker} or so, the value should be
+ * {@link Constants#NOT_A_COORDINATE}.If it's called on insertion from the
+ * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}.
*/
public void onCodeInput(int primaryCode, int x, int y);
- public static final int NOT_A_TOUCH_COORDINATE = -1;
- public static final int SUGGESTION_STRIP_COORDINATE = -2;
- public static final int SPELL_CHECKER_COORDINATE = -3;
-
/**
* Sends a sequence of characters to the listener.
*
@@ -64,6 +63,24 @@ public interface KeyboardActionListener {
public void onTextInput(CharSequence text);
/**
+ * Called when user started batch input.
+ */
+ public void onStartBatchInput();
+
+ /**
+ * Sends the ongoing batch input points data.
+ * @param batchPointers the batch input points representing the user input
+ */
+ public void onUpdateBatchInput(InputPointers batchPointers);
+
+ /**
+ * Sends the final batch input points data.
+ *
+ * @param batchPointers the batch input points representing the user input
+ */
+ public void onEndBatchInput(InputPointers batchPointers);
+
+ /**
* Called when user released a finger outside any key.
*/
public void onCancelInput();
@@ -84,10 +101,24 @@ public interface KeyboardActionListener {
@Override
public void onTextInput(CharSequence text) {}
@Override
+ public void onStartBatchInput() {}
+ @Override
+ public void onUpdateBatchInput(InputPointers batchPointers) {}
+ @Override
+ public void onEndBatchInput(InputPointers batchPointers) {}
+ @Override
public void onCancelInput() {}
@Override
public boolean onCustomRequest(int requestCode) {
return false;
}
+
+ // TODO: Remove this method when the vertical correction is removed.
+ public static boolean isInvalidCoordinate(int coordinate) {
+ // Detect {@link Constants#NOT_A_COORDINATE},
+ // {@link Constants#SUGGESTION_STRIP_COORDINATE}, and
+ // {@link Constants#SPELL_CHECKER_COORDINATE}.
+ return coordinate < 0;
+ }
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index 233716acf..5e8a8f6bb 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -33,7 +33,7 @@ import java.util.Locale;
/**
* Unique identifier for each keyboard type.
*/
-public class KeyboardId {
+public final class KeyboardId {
public static final int MODE_TEXT = 0;
public static final int MODE_URL = 1;
public static final int MODE_EMAIL = 2;
@@ -55,10 +55,15 @@ public class KeyboardId {
public static final int ELEMENT_PHONE_SYMBOLS = 8;
public static final int ELEMENT_NUMBER = 9;
+ public static final int FORM_FACTOR_PHONE = 0;
+ public static final int FORM_FACTOR_TABLET7 = 1;
+ public static final int FORM_FACTOR_TABLET10 = 2;
+
private static final int IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1;
public final InputMethodSubtype mSubtype;
public final Locale mLocale;
+ public final int mDeviceFormFactor;
public final int mOrientation;
public final int mWidth;
public final int mMode;
@@ -72,11 +77,12 @@ public class KeyboardId {
private final int mHashCode;
- public KeyboardId(int elementId, InputMethodSubtype subtype, int orientation, int width,
- int mode, EditorInfo editorInfo, boolean clobberSettingsKey, boolean shortcutKeyEnabled,
- boolean hasShortcutKey, boolean languageSwitchKeyEnabled) {
+ public KeyboardId(int elementId, InputMethodSubtype subtype, int deviceFormFactor,
+ int orientation, int width, int mode, EditorInfo editorInfo, boolean clobberSettingsKey,
+ boolean shortcutKeyEnabled, boolean hasShortcutKey, boolean languageSwitchKeyEnabled) {
mSubtype = subtype;
mLocale = SubtypeLocale.getSubtypeLocale(subtype);
+ mDeviceFormFactor = deviceFormFactor;
mOrientation = orientation;
mWidth = width;
mMode = mode;
@@ -94,6 +100,7 @@ public class KeyboardId {
private static int computeHashCode(KeyboardId id) {
return Arrays.hashCode(new Object[] {
+ id.mDeviceFormFactor,
id.mOrientation,
id.mElementId,
id.mMode,
@@ -115,7 +122,8 @@ public class KeyboardId {
private boolean equals(KeyboardId other) {
if (other == this)
return true;
- return other.mOrientation == mOrientation
+ return other.mDeviceFormFactor == mDeviceFormFactor
+ && other.mOrientation == mOrientation
&& other.mElementId == mElementId
&& other.mMode == mMode
&& other.mWidth == mWidth
@@ -137,11 +145,13 @@ public class KeyboardId {
}
public boolean navigateNext() {
- return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0;
+ return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0
+ || imeAction() == EditorInfo.IME_ACTION_NEXT;
}
public boolean navigatePrevious() {
- return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0;
+ return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0
+ || imeAction() == EditorInfo.IME_ACTION_PREVIOUS;
}
public boolean passwordInput() {
@@ -182,11 +192,11 @@ public class KeyboardId {
@Override
public String toString() {
- return String.format("[%s %s:%s %s%d %s %s %s%s%s%s%s%s%s%s]",
+ return String.format("[%s %s:%s %s-%s:%d %s %s %s%s%s%s%s%s%s%s]",
elementIdToName(mElementId),
mLocale,
mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
- (mOrientation == 1 ? "port" : "land"), mWidth,
+ deviceFormFactor(mDeviceFormFactor), (mOrientation == 1 ? "port" : "land"), mWidth,
modeName(mMode),
imeAction(),
(navigateNext() ? "navigateNext" : ""),
@@ -224,6 +234,15 @@ public class KeyboardId {
}
}
+ public static String deviceFormFactor(int devoceFormFactor) {
+ switch (devoceFormFactor) {
+ case FORM_FACTOR_PHONE: return "phone";
+ case FORM_FACTOR_TABLET7: return "tablet7";
+ case FORM_FACTOR_TABLET10: return "tablet10";
+ default: return null;
+ }
+ }
+
public static String modeName(int mode) {
switch (mode) {
case MODE_TEXT: return "text";
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index 8c7246855..c7813ab02 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -29,12 +29,16 @@ import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.text.InputType;
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.compat.EditorInfoCompatUtils;
-import com.android.inputmethod.keyboard.KeyboardLayoutSet.Params.ElementParams;
+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.CollectionUtils;
import com.android.inputmethod.latin.InputAttributes;
import com.android.inputmethod.latin.InputTypeUtils;
import com.android.inputmethod.latin.LatinImeLogger;
@@ -57,7 +61,7 @@ import java.util.HashMap;
* A {@link KeyboardLayoutSet} needs to be created for each
* {@link android.view.inputmethod.EditorInfo}.
*/
-public class KeyboardLayoutSet {
+public final class KeyboardLayoutSet {
private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG;
@@ -70,60 +74,41 @@ public class KeyboardLayoutSet {
private final Params mParams;
private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
- new HashMap<KeyboardId, SoftReference<Keyboard>>();
+ CollectionUtils.newHashMap();
private static final KeysCache sKeysCache = new KeysCache();
- public static class KeyboardLayoutSetException extends RuntimeException {
+ public static final class KeyboardLayoutSetException extends RuntimeException {
public final KeyboardId mKeyboardId;
- public KeyboardLayoutSetException(Throwable cause, KeyboardId keyboardId) {
+ public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
super(cause);
mKeyboardId = keyboardId;
}
}
- public static class KeysCache {
- private final HashMap<Key, Key> mMap;
-
- public KeysCache() {
- mMap = new HashMap<Key, Key>();
- }
-
- public void clear() {
- mMap.clear();
- }
-
- public Key get(Key key) {
- final Key existingKey = mMap.get(key);
- if (existingKey != null) {
- // Reuse the existing element that equals to "key" without adding "key" to the map.
- return existingKey;
- }
- mMap.put(key, key);
- return key;
- }
+ private static final class ElementParams {
+ int mKeyboardXmlId;
+ boolean mProximityCharsCorrectionEnabled;
+ public ElementParams() {}
}
- static class Params {
+ private static final class Params {
String mKeyboardLayoutSetName;
int mMode;
EditorInfo mEditorInfo;
- boolean mTouchPositionCorrectionEnabled;
+ boolean mDisableTouchPositionCorrectionDataForTest;
boolean mVoiceKeyEnabled;
boolean mVoiceKeyOnMain;
boolean mNoSettingsKey;
boolean mLanguageSwitchKeyEnabled;
InputMethodSubtype mSubtype;
+ int mDeviceFormFactor;
int mOrientation;
int mWidth;
- // KeyboardLayoutSet element id to element's parameters map.
- final HashMap<Integer, ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
- new HashMap<Integer, ElementParams>();
-
- static class ElementParams {
- int mKeyboardXmlId;
- boolean mProximityCharsCorrectionEnabled;
- }
+ // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
+ final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
+ CollectionUtils.newSparseArray();
+ public Params() {}
}
public static void clearKeyboardCache() {
@@ -131,12 +116,12 @@ public class KeyboardLayoutSet {
sKeysCache.clear();
}
- private KeyboardLayoutSet(Context context, Params params) {
+ KeyboardLayoutSet(final Context context, final Params params) {
mContext = context;
mParams = params;
}
- public Keyboard getKeyboard(int baseKeyboardLayoutSetElementId) {
+ public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
final int keyboardLayoutSetElementId;
switch (mParams.mMode) {
case KeyboardId.MODE_PHONE:
@@ -171,18 +156,20 @@ public class KeyboardLayoutSet {
}
}
- private Keyboard getKeyboard(ElementParams elementParams, final KeyboardId 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 Keyboard.Builder<Keyboard.Params> builder =
- new Keyboard.Builder<Keyboard.Params>(mContext, new Keyboard.Params());
+ 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);
- builder.setTouchPositionCorrectionEnabled(mParams.mTouchPositionCorrectionEnabled);
+ if (mParams.mDisableTouchPositionCorrectionDataForTest) {
+ builder.disableTouchPositionCorrectionDataForTest();
+ }
builder.setProximityCharsCorrectionEnabled(
elementParams.mProximityCharsCorrectionEnabled);
keyboard = builder.build();
@@ -203,19 +190,20 @@ public class KeyboardLayoutSet {
// KeyboardLayoutSet element id that is a key in keyboard_set.xml. Also that file specifies
// which XML layout should be used for each keyboard. The KeyboardId is an internal key for
// Keyboard object.
- private KeyboardId getKeyboardId(int keyboardLayoutSetElementId) {
+ private KeyboardId getKeyboardId(final int keyboardLayoutSetElementId) {
final Params params = mParams;
final boolean isSymbols = (keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS
|| keyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED);
final boolean noLanguage = SubtypeLocale.isNoLanguage(params.mSubtype);
final boolean voiceKeyEnabled = params.mVoiceKeyEnabled && !noLanguage;
final boolean hasShortcutKey = voiceKeyEnabled && (isSymbols != params.mVoiceKeyOnMain);
- return new KeyboardId(keyboardLayoutSetElementId, params.mSubtype, params.mOrientation,
- params.mWidth, params.mMode, params.mEditorInfo, params.mNoSettingsKey,
- voiceKeyEnabled, hasShortcutKey, params.mLanguageSwitchKeyEnabled);
+ return new KeyboardId(keyboardLayoutSetElementId, params.mSubtype, params.mDeviceFormFactor,
+ params.mOrientation, params.mWidth, params.mMode, params.mEditorInfo,
+ params.mNoSettingsKey, voiceKeyEnabled, hasShortcutKey,
+ params.mLanguageSwitchKeyEnabled);
}
- public static class Builder {
+ public static final class Builder {
private final Context mContext;
private final String mPackageName;
private final Resources mResources;
@@ -225,7 +213,7 @@ public class KeyboardLayoutSet {
private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
- public Builder(Context context, EditorInfo editorInfo) {
+ public Builder(final Context context, final EditorInfo editorInfo) {
mContext = context;
mPackageName = context.getPackageName();
mResources = context.getResources();
@@ -238,13 +226,16 @@ public class KeyboardLayoutSet {
mPackageName, NO_SETTINGS_KEY, mEditorInfo);
}
- public Builder setScreenGeometry(int orientation, int widthPixels) {
- mParams.mOrientation = orientation;
- mParams.mWidth = widthPixels;
+ public Builder setScreenGeometry(final int deviceFormFactor, final int orientation,
+ final int widthPixels) {
+ final Params params = mParams;
+ params.mDeviceFormFactor = deviceFormFactor;
+ params.mOrientation = orientation;
+ params.mWidth = widthPixels;
return this;
}
- public Builder setSubtype(InputMethodSubtype subtype) {
+ public Builder setSubtype(final InputMethodSubtype subtype) {
final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
@SuppressWarnings("deprecation")
final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
@@ -261,8 +252,8 @@ public class KeyboardLayoutSet {
return this;
}
- public Builder setOptions(boolean voiceKeyEnabled, boolean voiceKeyOnMain,
- boolean languageSwitchKeyEnabled) {
+ public Builder setOptions(final boolean voiceKeyEnabled, final boolean voiceKeyOnMain,
+ final boolean languageSwitchKeyEnabled) {
@SuppressWarnings("deprecation")
final boolean deprecatedNoMicrophone = InputAttributes.inPrivateImeOptions(
null, NO_MICROPHONE_COMPAT, mEditorInfo);
@@ -275,8 +266,9 @@ public class KeyboardLayoutSet {
return this;
}
- public void setTouchPositionCorrectionEnabled(boolean enabled) {
- mParams.mTouchPositionCorrectionEnabled = enabled;
+ // For test only
+ public void disableTouchPositionCorrectionDataForTest() {
+ mParams.mDisableTouchPositionCorrectionDataForTest = true;
}
public KeyboardLayoutSet build() {
@@ -296,7 +288,7 @@ public class KeyboardLayoutSet {
return new KeyboardLayoutSet(mContext, mParams);
}
- private void parseKeyboardLayoutSet(Resources res, int resId)
+ private void parseKeyboardLayoutSet(final Resources res, final int resId)
throws XmlPullParserException, IOException {
final XmlResourceParser parser = res.getXml(resId);
try {
@@ -316,7 +308,7 @@ public class KeyboardLayoutSet {
}
}
- private void parseKeyboardLayoutSetContent(XmlPullParser parser)
+ private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
throws XmlPullParserException, IOException {
int event;
while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
@@ -338,7 +330,7 @@ public class KeyboardLayoutSet {
}
}
- private void parseKeyboardLayoutSetElement(XmlPullParser parser)
+ private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
throws XmlPullParserException, IOException {
final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.KeyboardLayoutSet_Element);
@@ -365,7 +357,7 @@ public class KeyboardLayoutSet {
}
}
- private static int getKeyboardMode(EditorInfo editorInfo) {
+ private static int getKeyboardMode(final EditorInfo editorInfo) {
if (editorInfo == null)
return KeyboardId.MODE_TEXT;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 2e4ce199e..38025e8e4 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -21,7 +21,6 @@ import android.content.SharedPreferences;
import android.content.res.Resources;
import android.util.Log;
import android.view.ContextThemeWrapper;
-import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.EditorInfo;
@@ -38,32 +37,32 @@ import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.SettingsValues;
import com.android.inputmethod.latin.SubtypeSwitcher;
-import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.WordComposer;
-public class KeyboardSwitcher implements KeyboardState.SwitchActions {
+public final class KeyboardSwitcher implements KeyboardState.SwitchActions {
private static final String TAG = KeyboardSwitcher.class.getSimpleName();
public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20110916";
- static class KeyboardTheme {
- public final String mName;
+ static final class KeyboardTheme {
public final int mThemeId;
public final int mStyleId;
- public KeyboardTheme(String name, int themeId, int styleId) {
- mName = name;
+ // Note: The themeId should be aligned with "themeId" attribute of Keyboard style
+ // in values/style.xml.
+ public KeyboardTheme(int themeId, int styleId) {
mThemeId = themeId;
mStyleId = styleId;
}
}
private static final KeyboardTheme[] KEYBOARD_THEMES = {
- new KeyboardTheme("Basic", 0, R.style.KeyboardTheme),
- new KeyboardTheme("HighContrast", 1, R.style.KeyboardTheme_HighContrast),
- new KeyboardTheme("Stone", 6, R.style.KeyboardTheme_Stone),
- new KeyboardTheme("Stne.Bold", 7, R.style.KeyboardTheme_Stone_Bold),
- new KeyboardTheme("GingerBread", 8, R.style.KeyboardTheme_Gingerbread),
- new KeyboardTheme("IceCreamSandwich", 5, R.style.KeyboardTheme_IceCreamSandwich),
+ 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),
};
private SubtypeSwitcher mSubtypeSwitcher;
@@ -71,7 +70,7 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions {
private boolean mForceNonDistinctMultitouch;
private InputView mCurrentInputView;
- private LatinKeyboardView mKeyboardView;
+ private MainKeyboardView mKeyboardView;
private LatinIME mLatinIME;
private Resources mResources;
@@ -137,8 +136,9 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions {
public void loadKeyboard(EditorInfo editorInfo, SettingsValues settingsValues) {
final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
mThemeContext, editorInfo);
- builder.setScreenGeometry(mThemeContext.getResources().getConfiguration().orientation,
- mThemeContext.getResources().getDisplayMetrics().widthPixels);
+ final Resources res = mThemeContext.getResources();
+ builder.setScreenGeometry(res.getInteger(R.integer.config_device_form_factor),
+ res.getConfiguration().orientation, res.getDisplayMetrics().widthPixels);
builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype());
builder.setOptions(
settingsValues.isVoiceKeyEnabled(editorInfo),
@@ -169,19 +169,20 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
private void setKeyboard(final Keyboard keyboard) {
- final Keyboard oldKeyboard = mKeyboardView.getKeyboard();
- mKeyboardView.setKeyboard(keyboard);
+ final MainKeyboardView keyboardView = mKeyboardView;
+ final Keyboard oldKeyboard = keyboardView.getKeyboard();
+ keyboardView.setKeyboard(keyboard);
mCurrentInputView.setKeyboardGeometry(keyboard.mTopPadding);
- mKeyboardView.setKeyPreviewPopupEnabled(
+ keyboardView.setKeyPreviewPopupEnabled(
SettingsValues.isKeyPreviewPopupEnabled(mPrefs, mResources),
SettingsValues.getKeyPreviewPopupDismissDelay(mPrefs, mResources));
- mKeyboardView.updateAutoCorrectionState(mIsAutoCorrectionActive);
- mKeyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady());
+ keyboardView.updateAutoCorrectionState(mIsAutoCorrectionActive);
+ keyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady());
final boolean subtypeChanged = (oldKeyboard == null)
|| !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale);
final boolean needsToDisplayLanguage = mSubtypeSwitcher.needsToDisplayLanguage(
keyboard.mId.mLocale);
- mKeyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, needsToDisplayLanguage,
+ keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, needsToDisplayLanguage,
ImfUtils.hasMultipleEnabledIMEsOrSubtypes(mLatinIME, true));
}
@@ -265,7 +266,7 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions {
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void startDoubleTapTimer() {
- final LatinKeyboardView keyboardView = getKeyboardView();
+ final MainKeyboardView keyboardView = getMainKeyboardView();
if (keyboardView != null) {
final TimerProxy timer = keyboardView.getTimerProxy();
timer.startDoubleTapTimer();
@@ -275,7 +276,7 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions {
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void cancelDoubleTapTimer() {
- final LatinKeyboardView keyboardView = getKeyboardView();
+ final MainKeyboardView keyboardView = getMainKeyboardView();
if (keyboardView != null) {
final TimerProxy timer = keyboardView.getTimerProxy();
timer.cancelDoubleTapTimer();
@@ -285,7 +286,7 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions {
// Implements {@link KeyboardState.SwitchActions}.
@Override
public boolean isInDoubleTapTimeout() {
- final LatinKeyboardView keyboardView = getKeyboardView();
+ final MainKeyboardView keyboardView = getMainKeyboardView();
return (keyboardView != null)
? keyboardView.getTimerProxy().isInDoubleTapTimeout() : false;
}
@@ -293,7 +294,7 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions {
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void startLongPressTimer(int code) {
- final LatinKeyboardView keyboardView = getKeyboardView();
+ final MainKeyboardView keyboardView = getMainKeyboardView();
if (keyboardView != null) {
final TimerProxy timer = keyboardView.getTimerProxy();
timer.startLongPressTimer(code);
@@ -303,7 +304,7 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions {
// Implements {@link KeyboardState.SwitchActions}.
@Override
public void cancelLongPressTimer() {
- final LatinKeyboardView keyboardView = getKeyboardView();
+ final MainKeyboardView keyboardView = getMainKeyboardView();
if (keyboardView != null) {
final TimerProxy timer = keyboardView.getTimerProxy();
timer.cancelLongPressTimer();
@@ -343,33 +344,24 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions {
mState.onCodeInput(code, isSinglePointer(), mLatinIME.getCurrentAutoCapsState());
}
- public LatinKeyboardView getKeyboardView() {
+ public MainKeyboardView getMainKeyboardView() {
return mKeyboardView;
}
- public View onCreateInputView() {
+ public View onCreateInputView(boolean isHardwareAcceleratedDrawingEnabled) {
if (mKeyboardView != null) {
mKeyboardView.closing();
}
- Utils.GCUtils.getInstance().reset();
- boolean tryGC = true;
- for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
- try {
- setContextThemeWrapper(mLatinIME, mKeyboardTheme);
- mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
- R.layout.input_view, null);
- tryGC = false;
- } catch (OutOfMemoryError e) {
- Log.w(TAG, "load keyboard failed: " + e);
- tryGC = Utils.GCUtils.getInstance().tryGCOrWait(mKeyboardTheme.mName, e);
- } catch (InflateException e) {
- Log.w(TAG, "load keyboard failed: " + e);
- tryGC = Utils.GCUtils.getInstance().tryGCOrWait(mKeyboardTheme.mName, e);
- }
- }
+ setContextThemeWrapper(mLatinIME, mKeyboardTheme);
+ mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate(
+ R.layout.input_view, null);
- mKeyboardView = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.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.setKeyboardActionListener(mLatinIME);
if (mForceNonDistinctMultitouch) {
mKeyboardView.setDistinctMultitouch(false);
@@ -396,4 +388,22 @@ public class KeyboardSwitcher implements KeyboardState.SwitchActions {
}
}
}
+
+ public int getKeyboardShiftMode() {
+ final Keyboard keyboard = getKeyboard();
+ if (keyboard == null) {
+ return WordComposer.CAPS_MODE_OFF;
+ }
+ switch (keyboard.mId.mElementId) {
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
+ case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
+ return WordComposer.CAPS_MODE_MANUAL_SHIFT_LOCKED;
+ case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
+ return WordComposer.CAPS_MODE_MANUAL_SHIFTED;
+ case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
+ return WordComposer.CAPS_MODE_AUTO_SHIFTED;
+ default:
+ return WordComposer.CAPS_MODE_OFF;
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 51a0f537f..472f74b12 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -25,62 +25,96 @@ import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.PorterDuff;
import android.graphics.Rect;
-import android.graphics.Region.Op;
+import android.graphics.Region;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Message;
import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.SparseArray;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.RelativeLayout;
import android.widget.TextView;
+import com.android.inputmethod.keyboard.internal.KeyDrawParams;
+import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams;
+import com.android.inputmethod.keyboard.internal.KeyVisualAttributes;
+import com.android.inputmethod.keyboard.internal.PreviewPlacerView;
+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.StaticInnerHandlerWrapper;
import com.android.inputmethod.latin.StringUtils;
+import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
-import java.util.HashMap;
import java.util.HashSet;
/**
* A view that renders a virtual {@link Keyboard}.
*
- * @attr ref R.styleable#KeyboardView_backgroundDimAlpha
* @attr ref R.styleable#KeyboardView_keyBackground
- * @attr ref R.styleable#KeyboardView_keyLetterRatio
- * @attr ref R.styleable#KeyboardView_keyLargeLetterRatio
- * @attr ref R.styleable#KeyboardView_keyLabelRatio
- * @attr ref R.styleable#KeyboardView_keyHintLetterRatio
- * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintRatio
- * @attr ref R.styleable#KeyboardView_keyHintLabelRatio
+ * @attr ref R.styleable#KeyboardView_moreKeysLayout
+ * @attr ref R.styleable#KeyboardView_keyPreviewLayout
+ * @attr ref R.styleable#KeyboardView_keyPreviewOffset
+ * @attr ref R.styleable#KeyboardView_keyPreviewHeight
+ * @attr ref R.styleable#KeyboardView_keyPreviewLingerTimeout
* @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding
* @attr ref R.styleable#KeyboardView_keyHintLetterPadding
* @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding
* @attr ref R.styleable#KeyboardView_keyShiftedLetterHintPadding
- * @attr ref R.styleable#KeyboardView_keyTextStyle
- * @attr ref R.styleable#KeyboardView_keyPreviewLayout
- * @attr ref R.styleable#KeyboardView_keyPreviewTextRatio
- * @attr ref R.styleable#KeyboardView_keyPreviewOffset
- * @attr ref R.styleable#KeyboardView_keyPreviewHeight
- * @attr ref R.styleable#KeyboardView_keyTextColor
- * @attr ref R.styleable#KeyboardView_keyTextColorDisabled
- * @attr ref R.styleable#KeyboardView_keyHintLetterColor
- * @attr ref R.styleable#KeyboardView_keyHintLabelColor
- * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintInactivatedColor
- * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintActivatedColor
- * @attr ref R.styleable#KeyboardView_shadowColor
- * @attr ref R.styleable#KeyboardView_shadowRadius
+ * @attr ref R.styleable#KeyboardView_keyTextShadowRadius
+ * @attr ref R.styleable#KeyboardView_backgroundDimAlpha
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextSize
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextColor
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextOffset
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewColor
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewHorizontalPadding
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewVerticalPadding
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewRoundRadius
+ * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextLingerTimeout
+ * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutStartDelay
+ * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutDuration
+ * @attr ref R.styleable#KeyboardView_gesturePreviewTrailUpdateInterval
+ * @attr ref R.styleable#KeyboardView_gesturePreviewTrailColor
+ * @attr ref R.styleable#KeyboardView_gesturePreviewTrailWidth
+ * @attr ref R.styleable#KeyboardView_verticalCorrection
+ * @attr ref R.styleable#Keyboard_Key_keyTypeface
+ * @attr ref R.styleable#Keyboard_Key_keyLetterSize
+ * @attr ref R.styleable#Keyboard_Key_keyLabelSize
+ * @attr ref R.styleable#Keyboard_Key_keyLargeLetterRatio
+ * @attr ref R.styleable#Keyboard_Key_keyLargeLabelRatio
+ * @attr ref R.styleable#Keyboard_Key_keyHintLetterRatio
+ * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintRatio
+ * @attr ref R.styleable#Keyboard_Key_keyHintLabelRatio
+ * @attr ref R.styleable#Keyboard_Key_keyPreviewTextRatio
+ * @attr ref R.styleable#Keyboard_Key_keyTextColor
+ * @attr ref R.styleable#Keyboard_Key_keyTextColorDisabled
+ * @attr ref R.styleable#Keyboard_Key_keyTextShadowColor
+ * @attr ref R.styleable#Keyboard_Key_keyHintLetterColor
+ * @attr ref R.styleable#Keyboard_Key_keyHintLabelColor
+ * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintInactivatedColor
+ * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintActivatedColor
+ * @attr ref R.styleable#Keyboard_Key_keyPreviewTextColor
*/
public class KeyboardView extends View implements PointerTracker.DrawingProxy {
- // Miscellaneous constants
- private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
+ private static final String TAG = KeyboardView.class.getSimpleName();
// XML attributes
+ protected final KeyVisualAttributes mKeyVisualAttributes;
+ private final int mKeyLabelHorizontalPadding;
+ private final float mKeyHintLetterPadding;
+ private final float mKeyPopupHintLetterPadding;
+ private final float mKeyShiftedLetterHintPadding;
+ private final float mKeyTextShadowRadius;
protected final float mVerticalCorrection;
protected final int mMoreKeysLayout;
+ protected final Drawable mKeyBackground;
+ protected final Rect mKeyBackgroundPadding = new Rect();
private final int mBackgroundDimAlpha;
// HORIZONTAL ELLIPSIS "...", character for popup hint.
@@ -94,75 +128,104 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
// The maximum key label width in the proportion to the key width.
private static final float MAX_LABEL_RATIO = 0.90f;
- private final static int ALPHA_OPAQUE = 255;
-
// Main keyboard
private Keyboard mKeyboard;
- protected final KeyDrawParams mKeyDrawParams;
+ protected final KeyDrawParams mKeyDrawParams = new KeyDrawParams();
+
+ // Preview placer view
+ private final PreviewPlacerView mPreviewPlacerView;
+ private final int[] mCoordinates = new int[2];
// Key preview
+ private static final int PREVIEW_ALPHA = 240;
private final int mKeyPreviewLayoutId;
- protected final KeyPreviewDrawParams mKeyPreviewDrawParams;
+ private final int mPreviewOffset;
+ private final int mPreviewHeight;
+ private final int mPreviewLingerTimeout;
+ private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray();
+ protected final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams();
private boolean mShowKeyPreviewPopup = true;
private int mDelayAfterPreview;
- private ViewGroup mPreviewPlacer;
+ // Background state set
+ private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = {
+ { // STATE_MIDDLE
+ EMPTY_STATE_SET,
+ { R.attr.state_has_morekeys }
+ },
+ { // STATE_LEFT
+ { R.attr.state_left_edge },
+ { R.attr.state_left_edge, R.attr.state_has_morekeys }
+ },
+ { // STATE_RIGHT
+ { R.attr.state_right_edge },
+ { R.attr.state_right_edge, R.attr.state_has_morekeys }
+ }
+ };
+ private static final int STATE_MIDDLE = 0;
+ private static final int STATE_LEFT = 1;
+ private static final int STATE_RIGHT = 2;
+ private static final int STATE_NORMAL = 0;
+ private static final int STATE_HAS_MOREKEYS = 1;
+ private static final int[] KEY_PREVIEW_BACKGROUND_DEFAULT_STATE =
+ KEY_PREVIEW_BACKGROUND_STATE_TABLE[STATE_MIDDLE][STATE_NORMAL];
// Drawing
/** True if the entire keyboard needs to be dimmed. */
private boolean mNeedsToDimEntireKeyboard;
- /** Whether the keyboard bitmap buffer needs to be redrawn before it's blitted. **/
- private boolean mBufferNeedsUpdate;
/** True if all keys should be drawn */
private boolean mInvalidateAllKeys;
/** The keys that should be drawn */
- private final HashSet<Key> mInvalidatedKeys = new HashSet<Key>();
- /** The region of invalidated keys */
- private final Rect mInvalidatedKeysRect = new Rect();
+ private final HashSet<Key> mInvalidatedKeys = CollectionUtils.newHashSet();
+ /** The working rectangle variable */
+ private final Rect mWorkingRect = new Rect();
/** The keyboard bitmap buffer for faster updates */
- private Bitmap mBuffer;
+ /** The clip region to draw keys */
+ private final Region mClipRegion = new Region();
+ private Bitmap mOffscreenBuffer;
/** The canvas for the above mutable keyboard bitmap */
- private Canvas mCanvas;
+ private final Canvas mOffscreenCanvas = new Canvas();
private final Paint mPaint = new Paint();
private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics();
- // This map caches key label text height in pixel as value and key label text size as map key.
- private static final HashMap<Integer, Float> sTextHeightCache =
- new HashMap<Integer, Float>();
- // This map caches key label text width in pixel as value and key label text size as map key.
- private static final HashMap<Integer, Float> sTextWidthCache =
- new HashMap<Integer, Float>();
+ // This sparse array caches key label text height in pixel indexed by key label text size.
+ private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray();
+ // This sparse array caches key label text width in pixel indexed by key label text size.
+ private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray();
private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' };
private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' };
private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> {
- private static final int MSG_DISMISS_KEY_PREVIEW = 1;
+ private static final int MSG_DISMISS_KEY_PREVIEW = 0;
- public DrawingHandler(KeyboardView outerInstance) {
+ public DrawingHandler(final KeyboardView outerInstance) {
super(outerInstance);
}
@Override
- public void handleMessage(Message msg) {
+ public void handleMessage(final Message msg) {
final KeyboardView keyboardView = getOuterInstance();
if (keyboardView == null) return;
final PointerTracker tracker = (PointerTracker) msg.obj;
switch (msg.what) {
case MSG_DISMISS_KEY_PREVIEW:
- tracker.getKeyPreviewText().setVisibility(View.INVISIBLE);
+ final TextView previewText = keyboardView.mKeyPreviewTexts.get(tracker.mPointerId);
+ if (previewText != null) {
+ previewText.setVisibility(INVISIBLE);
+ }
break;
}
}
- public void dismissKeyPreview(long delay, PointerTracker tracker) {
+ public void dismissKeyPreview(final long delay, final PointerTracker tracker) {
sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
}
- public void cancelDismissKeyPreview(PointerTracker tracker) {
+ public void cancelDismissKeyPreview(final PointerTracker tracker) {
removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
}
- public void cancelAllDismissKeyPreviews() {
+ private void cancelAllDismissKeyPreviews() {
removeMessages(MSG_DISMISS_KEY_PREVIEW);
}
@@ -171,214 +234,60 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
}
}
- protected static class KeyDrawParams {
- // XML attributes
- public final int mKeyTextColor;
- public final int mKeyTextInactivatedColor;
- public final Typeface mKeyTextStyle;
- public final float mKeyLabelHorizontalPadding;
- public final float mKeyHintLetterPadding;
- public final float mKeyPopupHintLetterPadding;
- public final float mKeyShiftedLetterHintPadding;
- public final int mShadowColor;
- public final float mShadowRadius;
- public final Drawable mKeyBackground;
- public final int mKeyHintLetterColor;
- public final int mKeyHintLabelColor;
- public final int mKeyShiftedLetterHintInactivatedColor;
- public final int mKeyShiftedLetterHintActivatedColor;
-
- /* package */ final float mKeyLetterRatio;
- private final float mKeyLargeLetterRatio;
- private final float mKeyLabelRatio;
- private final float mKeyLargeLabelRatio;
- private final float mKeyHintLetterRatio;
- private final float mKeyShiftedLetterHintRatio;
- private final float mKeyHintLabelRatio;
- private static final float UNDEFINED_RATIO = -1.0f;
-
- public final Rect mPadding = new Rect();
- public int mKeyLetterSize;
- public int mKeyLargeLetterSize;
- public int mKeyLabelSize;
- public int mKeyLargeLabelSize;
- public int mKeyHintLetterSize;
- public int mKeyShiftedLetterHintSize;
- public int mKeyHintLabelSize;
- public int mAnimAlpha;
-
- public KeyDrawParams(TypedArray a) {
- mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground);
- if (a.hasValue(R.styleable.KeyboardView_keyLetterSize)) {
- mKeyLetterRatio = UNDEFINED_RATIO;
- mKeyLetterSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLetterSize, 0);
- } else {
- mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio);
- }
- if (a.hasValue(R.styleable.KeyboardView_keyLabelSize)) {
- mKeyLabelRatio = UNDEFINED_RATIO;
- mKeyLabelSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLabelSize, 0);
- } else {
- mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio);
- }
- mKeyLargeLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLabelRatio);
- mKeyLargeLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLetterRatio);
- mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio);
- mKeyShiftedLetterHintRatio = getRatio(a,
- R.styleable.KeyboardView_keyShiftedLetterHintRatio);
- mKeyHintLabelRatio = getRatio(a, R.styleable.KeyboardView_keyHintLabelRatio);
- mKeyLabelHorizontalPadding = a.getDimension(
- R.styleable.KeyboardView_keyLabelHorizontalPadding, 0);
- mKeyHintLetterPadding = a.getDimension(
- R.styleable.KeyboardView_keyHintLetterPadding, 0);
- mKeyPopupHintLetterPadding = a.getDimension(
- R.styleable.KeyboardView_keyPopupHintLetterPadding, 0);
- mKeyShiftedLetterHintPadding = a.getDimension(
- R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0);
- mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000);
- mKeyTextInactivatedColor = a.getColor(
- R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000);
- mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0);
- mKeyHintLabelColor = a.getColor(R.styleable.KeyboardView_keyHintLabelColor, 0);
- mKeyShiftedLetterHintInactivatedColor = a.getColor(
- R.styleable.KeyboardView_keyShiftedLetterHintInactivatedColor, 0);
- mKeyShiftedLetterHintActivatedColor = a.getColor(
- R.styleable.KeyboardView_keyShiftedLetterHintActivatedColor, 0);
- mKeyTextStyle = Typeface.defaultFromStyle(
- a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL));
- mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0);
- mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f);
-
- mKeyBackground.getPadding(mPadding);
- }
-
- public void updateKeyHeight(int keyHeight) {
- if (mKeyLetterRatio >= 0.0f)
- mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
- if (mKeyLabelRatio >= 0.0f)
- mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio);
- mKeyLargeLabelSize = (int)(keyHeight * mKeyLargeLabelRatio);
- mKeyLargeLetterSize = (int)(keyHeight * mKeyLargeLetterRatio);
- mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio);
- mKeyShiftedLetterHintSize = (int)(keyHeight * mKeyShiftedLetterHintRatio);
- mKeyHintLabelSize = (int)(keyHeight * mKeyHintLabelRatio);
- }
-
- public void blendAlpha(Paint paint) {
- final int color = paint.getColor();
- paint.setARGB((paint.getAlpha() * mAnimAlpha) / ALPHA_OPAQUE,
- Color.red(color), Color.green(color), Color.blue(color));
- }
- }
-
- /* package */ static class KeyPreviewDrawParams {
- // XML attributes.
- public final Drawable mPreviewBackground;
- public final Drawable mPreviewLeftBackground;
- public final Drawable mPreviewRightBackground;
- public final int mPreviewTextColor;
- public final int mPreviewOffset;
- public final int mPreviewHeight;
- public final Typeface mKeyTextStyle;
- public final int mLingerTimeout;
-
- private final float mPreviewTextRatio;
- private final float mKeyLetterRatio;
-
- // The graphical geometry of the key preview.
- // <-width->
- // +-------+ ^
- // | | |
- // |preview| height (visible)
- // | | |
- // + + ^ v
- // \ / |offset
- // +-\ /-+ v
- // | +-+ |
- // |parent |
- // | key|
- // +-------+
- // The background of a {@link TextView} being used for a key preview may have invisible
- // paddings. To align the more keys keyboard panel's visible part with the visible part of
- // the background, we need to record the width and height of key preview that don't include
- // invisible paddings.
- public int mPreviewVisibleWidth;
- public int mPreviewVisibleHeight;
- // The key preview may have an arbitrary offset and its background that may have a bottom
- // padding. To align the more keys keyboard and the key preview we also need to record the
- // offset between the top edge of parent key and the bottom of the visible part of key
- // preview background.
- public int mPreviewVisibleOffset;
-
- public int mPreviewTextSize;
- public int mKeyLetterSize;
- public final int[] mCoordinates = new int[2];
-
- private static final int PREVIEW_ALPHA = 240;
-
- public KeyPreviewDrawParams(TypedArray a, KeyDrawParams keyDrawParams) {
- mPreviewBackground = a.getDrawable(R.styleable.KeyboardView_keyPreviewBackground);
- mPreviewLeftBackground = a.getDrawable(
- R.styleable.KeyboardView_keyPreviewLeftBackground);
- mPreviewRightBackground = a.getDrawable(
- R.styleable.KeyboardView_keyPreviewRightBackground);
- setAlpha(mPreviewBackground, PREVIEW_ALPHA);
- setAlpha(mPreviewLeftBackground, PREVIEW_ALPHA);
- setAlpha(mPreviewRightBackground, PREVIEW_ALPHA);
- mPreviewOffset = a.getDimensionPixelOffset(
- R.styleable.KeyboardView_keyPreviewOffset, 0);
- mPreviewHeight = a.getDimensionPixelSize(
- R.styleable.KeyboardView_keyPreviewHeight, 80);
- mPreviewTextRatio = getRatio(a, R.styleable.KeyboardView_keyPreviewTextRatio);
- mPreviewTextColor = a.getColor(R.styleable.KeyboardView_keyPreviewTextColor, 0);
- mLingerTimeout = a.getInt(R.styleable.KeyboardView_keyPreviewLingerTimeout, 0);
-
- mKeyLetterRatio = keyDrawParams.mKeyLetterRatio;
- mKeyTextStyle = keyDrawParams.mKeyTextStyle;
- }
-
- public void updateKeyHeight(int keyHeight) {
- mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio);
- mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio);
- }
-
- private static void setAlpha(Drawable drawable, int alpha) {
- if (drawable == null)
- return;
- drawable.setAlpha(alpha);
- }
- }
-
- public KeyboardView(Context context, AttributeSet attrs) {
+ public KeyboardView(final Context context, final AttributeSet attrs) {
this(context, attrs, R.attr.keyboardViewStyle);
}
- public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
+ public KeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
- final TypedArray a = context.obtainStyledAttributes(
- attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
-
- mKeyDrawParams = new KeyDrawParams(a);
- mKeyPreviewDrawParams = new KeyPreviewDrawParams(a, mKeyDrawParams);
- mKeyPreviewLayoutId = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0);
+ final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs,
+ R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
+ mKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground);
+ mKeyBackground.getPadding(mKeyBackgroundPadding);
+ mPreviewOffset = keyboardViewAttr.getDimensionPixelOffset(
+ R.styleable.KeyboardView_keyPreviewOffset, 0);
+ mPreviewHeight = keyboardViewAttr.getDimensionPixelSize(
+ R.styleable.KeyboardView_keyPreviewHeight, 80);
+ mPreviewLingerTimeout = keyboardViewAttr.getInt(
+ R.styleable.KeyboardView_keyPreviewLingerTimeout, 0);
+ mDelayAfterPreview = mPreviewLingerTimeout;
+ mKeyLabelHorizontalPadding = keyboardViewAttr.getDimensionPixelOffset(
+ R.styleable.KeyboardView_keyLabelHorizontalPadding, 0);
+ mKeyHintLetterPadding = keyboardViewAttr.getDimension(
+ R.styleable.KeyboardView_keyHintLetterPadding, 0);
+ mKeyPopupHintLetterPadding = keyboardViewAttr.getDimension(
+ R.styleable.KeyboardView_keyPopupHintLetterPadding, 0);
+ mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension(
+ R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0);
+ mKeyTextShadowRadius = keyboardViewAttr.getFloat(
+ R.styleable.KeyboardView_keyTextShadowRadius, 0.0f);
+ mKeyPreviewLayoutId = keyboardViewAttr.getResourceId(
+ R.styleable.KeyboardView_keyPreviewLayout, 0);
if (mKeyPreviewLayoutId == 0) {
mShowKeyPreviewPopup = false;
}
- mVerticalCorrection = a.getDimensionPixelOffset(
+ mVerticalCorrection = keyboardViewAttr.getDimension(
R.styleable.KeyboardView_verticalCorrection, 0);
- mMoreKeysLayout = a.getResourceId(R.styleable.KeyboardView_moreKeysLayout, 0);
- mBackgroundDimAlpha = a.getInt(R.styleable.KeyboardView_backgroundDimAlpha, 0);
- a.recycle();
-
- mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout;
-
+ mMoreKeysLayout = keyboardViewAttr.getResourceId(
+ R.styleable.KeyboardView_moreKeysLayout, 0);
+ mBackgroundDimAlpha = keyboardViewAttr.getInt(
+ R.styleable.KeyboardView_backgroundDimAlpha, 0);
+ keyboardViewAttr.recycle();
+
+ final TypedArray keyAttr = context.obtainStyledAttributes(attrs,
+ R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView);
+ mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
+ keyAttr.recycle();
+
+ mPreviewPlacerView = new PreviewPlacerView(context, attrs);
mPaint.setAntiAlias(true);
}
- // Read fraction value in TypedArray as float.
- /* package */ static float getRatio(TypedArray a, int index) {
- return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
+ private static void blendAlpha(final Paint paint, final int alpha) {
+ final int color = paint.getColor();
+ paint.setARGB((paint.getAlpha() * alpha) / Constants.Color.ALPHA_OPAQUE,
+ Color.red(color), Color.green(color), Color.blue(color));
}
/**
@@ -388,14 +297,14 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
* @see #getKeyboard()
* @param keyboard the keyboard to display in this view
*/
- public void setKeyboard(Keyboard keyboard) {
+ public void setKeyboard(final Keyboard keyboard) {
mKeyboard = keyboard;
LatinImeLogger.onSetKeyboard(keyboard);
requestLayout();
invalidateAllKeys();
final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
- mKeyDrawParams.updateKeyHeight(keyHeight);
- mKeyPreviewDrawParams.updateKeyHeight(keyHeight);
+ mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes);
+ mKeyDrawParams.updateParams(keyHeight, keyboard.mKeyVisualAttributes);
}
/**
@@ -414,7 +323,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
* @param delay the delay after which the preview is dismissed
* @see #isKeyPreviewPopupEnabled()
*/
- public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
+ public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) {
mShowKeyPreviewPopup = previewEnabled;
mDelayAfterPreview = delay;
}
@@ -428,8 +337,14 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
return mShowKeyPreviewPopup;
}
+ public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail,
+ final boolean drawsGestureFloatingPreviewText) {
+ mPreviewPlacerView.setGesturePreviewMode(
+ drawsGesturePreviewTrail, drawsGestureFloatingPreviewText);
+ }
+
@Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ 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();
@@ -440,73 +355,116 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
}
@Override
- public void onDraw(Canvas canvas) {
+ public void onDraw(final Canvas canvas) {
super.onDraw(canvas);
- if (mBufferNeedsUpdate || mBuffer == null) {
- mBufferNeedsUpdate = false;
- onBufferDraw();
+ if (canvas.isHardwareAccelerated()) {
+ onDrawKeyboard(canvas);
+ return;
+ }
+
+ final boolean bufferNeedsUpdates = mInvalidateAllKeys || !mInvalidatedKeys.isEmpty();
+ if (bufferNeedsUpdates || mOffscreenBuffer == null) {
+ if (maybeAllocateOffscreenBuffer()) {
+ mInvalidateAllKeys = true;
+ // TODO: Stop using the offscreen canvas even when in software rendering
+ mOffscreenCanvas.setBitmap(mOffscreenBuffer);
+ }
+ onDrawKeyboard(mOffscreenCanvas);
}
- canvas.drawBitmap(mBuffer, 0, 0, null);
+ canvas.drawBitmap(mOffscreenBuffer, 0, 0, null);
}
- private void onBufferDraw() {
+ private boolean maybeAllocateOffscreenBuffer() {
final int width = getWidth();
final int height = getHeight();
- if (width == 0 || height == 0)
- return;
- if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) {
- if (mBuffer != null)
- mBuffer.recycle();
- mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- mInvalidateAllKeys = true;
- if (mCanvas != null) {
- mCanvas.setBitmap(mBuffer);
- } else {
- mCanvas = new Canvas(mBuffer);
- }
+ if (width == 0 || height == 0) {
+ return false;
+ }
+ if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == width
+ && mOffscreenBuffer.getHeight() == height) {
+ return false;
+ }
+ freeOffscreenBuffer();
+ mOffscreenBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ return true;
+ }
+
+ private void freeOffscreenBuffer() {
+ if (mOffscreenBuffer != null) {
+ mOffscreenBuffer.recycle();
+ mOffscreenBuffer = null;
}
+ }
+ private void onDrawKeyboard(final Canvas canvas) {
if (mKeyboard == null) return;
- final Canvas canvas = mCanvas;
+ final int width = getWidth();
+ final int height = getHeight();
final Paint paint = mPaint;
- final KeyDrawParams params = mKeyDrawParams;
- if (mInvalidateAllKeys || mInvalidatedKeys.isEmpty()) {
- mInvalidatedKeysRect.set(0, 0, width, height);
- canvas.clipRect(mInvalidatedKeysRect, Op.REPLACE);
+ // Calculate clip region and set.
+ final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty();
+ final boolean isHardwareAccelerated = canvas.isHardwareAccelerated();
+ // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on.
+ if (drawAllKeys || isHardwareAccelerated) {
+ mClipRegion.set(0, 0, width, height);
+ } else {
+ 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);
+ mClipRegion.union(mWorkingRect);
+ }
+ }
+ }
+ if (!isHardwareAccelerated) {
+ canvas.clipRegion(mClipRegion, Region.Op.REPLACE);
+ // Draw keyboard background.
canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
+ final Drawable background = getBackground();
+ if (background != null) {
+ background.draw(canvas);
+ }
+ }
+
+ // 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) {
- onDrawKey(key, canvas, paint, params);
- }
- if (mNeedsToDimEntireKeyboard) {
- drawDimRectangle(canvas, mInvalidatedKeysRect, mBackgroundDimAlpha, paint);
+ onDrawKey(key, canvas, paint);
}
} else {
// Draw invalidated keys.
for (final Key key : mInvalidatedKeys) {
- if (!mKeyboard.hasKey(key)) {
- continue;
- }
- final int x = key.mX + getPaddingLeft();
- final int y = key.mY + getPaddingTop();
- mInvalidatedKeysRect.set(x, y, x + key.mWidth, y + key.mHeight);
- canvas.clipRect(mInvalidatedKeysRect, Op.REPLACE);
- canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
- onDrawKey(key, canvas, paint, params);
- if (mNeedsToDimEntireKeyboard) {
- drawDimRectangle(canvas, mInvalidatedKeysRect, mBackgroundDimAlpha, paint);
+ if (mKeyboard.hasKey(key)) {
+ onDrawKey(key, canvas, paint);
}
}
}
+ // Overlay a dark rectangle to dim.
+ if (mNeedsToDimEntireKeyboard) {
+ paint.setColor(Color.BLACK);
+ paint.setAlpha(mBackgroundDimAlpha);
+ // Note: clipRegion() above is in effect if it was called.
+ canvas.drawRect(0, 0, width, height, paint);
+ }
+
+ // ResearchLogging indicator.
+ // TODO: Reimplement using a keyboard background image specific to the ResearchLogger,
+ // and remove this call.
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().paintIndicator(this, paint, canvas, width, height);
+ }
+
mInvalidatedKeys.clear();
- mInvalidatedKeysRect.setEmpty();
mInvalidateAllKeys = false;
}
- public void dimEntireKeyboard(boolean dimmed) {
+ public void dimEntireKeyboard(final boolean dimmed) {
final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed;
mNeedsToDimEntireKeyboard = dimmed;
if (needsRedrawing) {
@@ -514,14 +472,18 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
}
}
- private void onDrawKey(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
- final int keyDrawX = key.mX + key.mVisualInsetsLeft + getPaddingLeft();
+ private void onDrawKey(final Key key, final Canvas canvas, final Paint paint) {
+ final int keyDrawX = key.getDrawX() + getPaddingLeft();
final int keyDrawY = key.mY + getPaddingTop();
canvas.translate(keyDrawX, keyDrawY);
- params.mAnimAlpha = ALPHA_OPAQUE;
+ final int keyHeight = mKeyboard.mMostCommonKeyHeight - mKeyboard.mVerticalGap;
+ final KeyVisualAttributes attr = key.mKeyVisualAttributes;
+ final KeyDrawParams params = mKeyDrawParams.mayCloneAndUpdateParams(keyHeight, attr);
+ params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE;
+
if (!key.isSpacer()) {
- onDrawKeyBackground(key, canvas, params);
+ onDrawKeyBackground(key, canvas);
}
onDrawKeyTopVisuals(key, canvas, paint, params);
@@ -529,14 +491,14 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
}
// Draw key background.
- protected void onDrawKeyBackground(Key key, Canvas canvas, KeyDrawParams params) {
- final int bgWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight
- + params.mPadding.left + params.mPadding.right;
- final int bgHeight = key.mHeight + params.mPadding.top + params.mPadding.bottom;
- final int bgX = -params.mPadding.left;
- final int bgY = -params.mPadding.top;
+ 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 bgX = -padding.left;
+ final int bgY = -padding.top;
final int[] drawableState = key.getCurrentDrawableState();
- final Drawable background = params.mKeyBackground;
+ final Drawable background = mKeyBackground;
background.setState(drawableState);
final Rect bounds = background.getBounds();
if (bgWidth != bounds.right || bgHeight != bounds.bottom) {
@@ -551,8 +513,9 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
}
// Draw key top visuals.
- protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
- final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
+ 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 float centerX = keyWidth * 0.5f;
final float centerY = keyHeight * 0.5f;
@@ -566,12 +529,8 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
float positionX = centerX;
if (key.mLabel != null) {
final String label = key.mLabel;
- // For characters, use large font. For labels like "Done", use smaller font.
- paint.setTypeface(key.selectTypeface(params.mKeyTextStyle));
- final int labelSize = key.selectTextSize(params.mKeyLetterSize,
- params.mKeyLargeLetterSize, params.mKeyLabelSize, params.mKeyLargeLabelSize,
- params.mKeyHintLabelSize);
- paint.setTextSize(labelSize);
+ paint.setTypeface(key.selectTypeface(params));
+ paint.setTextSize(key.selectTextSize(params));
final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint);
final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint);
@@ -581,10 +540,10 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
// Horizontal label text alignment
float labelWidth = 0;
if (key.isAlignLeft()) {
- positionX = (int)params.mKeyLabelHorizontalPadding;
+ positionX = mKeyLabelHorizontalPadding;
paint.setTextAlign(Align.LEFT);
} else if (key.isAlignRight()) {
- positionX = keyWidth - (int)params.mKeyLabelHorizontalPadding;
+ positionX = keyWidth - mKeyLabelHorizontalPadding;
paint.setTextAlign(Align.RIGHT);
} else if (key.isAlignLeftOfCenter()) {
// TODO: Parameterise this?
@@ -609,16 +568,15 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint)));
}
- paint.setColor(key.isShiftedLetterActivated()
- ? params.mKeyTextInactivatedColor : params.mKeyTextColor);
+ paint.setColor(key.selectTextColor(params));
if (key.isEnabled()) {
// Set a drop shadow for the text
- paint.setShadowLayer(params.mShadowRadius, 0, 0, params.mShadowColor);
+ paint.setShadowLayer(mKeyTextShadowRadius, 0, 0, params.mTextShadowColor);
} else {
// Make label invisible
paint.setColor(Color.TRANSPARENT);
}
- params.blendAlpha(paint);
+ blendAlpha(paint, params.mAnimAlpha);
canvas.drawText(label, 0, label.length(), positionX, baseline, paint);
// Turn off drop shadow and reset x-scale.
paint.setShadowLayer(0, 0, 0, 0);
@@ -646,25 +604,10 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
// Draw hint label.
if (key.mHintLabel != null) {
- final String hint = key.mHintLabel;
- final int hintColor;
- final int hintSize;
- if (key.hasHintLabel()) {
- hintColor = params.mKeyHintLabelColor;
- hintSize = params.mKeyHintLabelSize;
- paint.setTypeface(Typeface.DEFAULT);
- } else if (key.hasShiftedLetterHint()) {
- hintColor = key.isShiftedLetterActivated()
- ? params.mKeyShiftedLetterHintActivatedColor
- : params.mKeyShiftedLetterHintInactivatedColor;
- hintSize = params.mKeyShiftedLetterHintSize;
- } else { // key.hasHintLetter()
- hintColor = params.mKeyHintLetterColor;
- hintSize = params.mKeyHintLetterSize;
- }
- paint.setColor(hintColor);
- params.blendAlpha(paint);
- paint.setTextSize(hintSize);
+ final String hintLabel = key.mHintLabel;
+ paint.setTextSize(key.selectHintTextSize(params));
+ paint.setColor(key.selectHintTextColor(params));
+ blendAlpha(paint, params.mAnimAlpha);
final float hintX, hintY;
if (key.hasHintLabel()) {
// The hint label is placed just right of the key label. Used mainly on
@@ -675,19 +618,19 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
paint.setTextAlign(Align.LEFT);
} else if (key.hasShiftedLetterHint()) {
// The hint label is placed at top-right corner of the key. Used mainly on tablet.
- hintX = keyWidth - params.mKeyShiftedLetterHintPadding
+ hintX = keyWidth - mKeyShiftedLetterHintPadding
- getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
paint.getFontMetrics(mFontMetrics);
hintY = -mFontMetrics.top;
paint.setTextAlign(Align.CENTER);
} else { // key.hasHintLetter()
// The hint letter is placed at top-right corner of the key. Used mainly on phone.
- hintX = keyWidth - params.mKeyHintLetterPadding
+ hintX = keyWidth - mKeyHintLetterPadding
- getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2;
hintY = -paint.ascent();
paint.setTextAlign(Align.CENTER);
}
- canvas.drawText(hint, 0, hint.length(), hintX, hintY, paint);
+ canvas.drawText(hintLabel, 0, hintLabel.length(), hintX, hintY, paint);
if (LatinImeLogger.sVISUALDEBUG) {
final Paint line = new Paint();
@@ -698,15 +641,15 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
// Draw key icon.
if (key.mLabel == null && icon != null) {
- final int iconWidth = icon.getIntrinsicWidth();
+ final int iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth);
final int iconHeight = icon.getIntrinsicHeight();
final int iconX, alignX;
final int iconY = (keyHeight - iconHeight) / 2;
if (key.isAlignLeft()) {
- iconX = (int)params.mKeyLabelHorizontalPadding;
+ iconX = mKeyLabelHorizontalPadding;
alignX = iconX;
} else if (key.isAlignRight()) {
- iconX = keyWidth - (int)params.mKeyLabelHorizontalPadding - iconWidth;
+ iconX = keyWidth - mKeyLabelHorizontalPadding - iconWidth;
alignX = iconX + iconWidth;
} else { // Align center
iconX = (keyWidth - iconWidth) / 2;
@@ -727,17 +670,18 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
}
// Draw popup hint "..." at the bottom right corner of the key.
- protected void drawKeyPopupHint(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
- final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
+ 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;
- paint.setTypeface(params.mKeyTextStyle);
- paint.setTextSize(params.mKeyHintLetterSize);
- paint.setColor(params.mKeyHintLabelColor);
+ paint.setTypeface(params.mTypeface);
+ paint.setTextSize(params.mHintLetterSize);
+ paint.setColor(params.mHintLabelColor);
paint.setTextAlign(Align.CENTER);
- final float hintX = keyWidth - params.mKeyHintLetterPadding
+ final float hintX = keyWidth - mKeyHintLetterPadding
- getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2;
- final float hintY = keyHeight - params.mKeyPopupHintLetterPadding;
+ final float hintY = keyHeight - mKeyPopupHintLetterPadding;
canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint);
if (LatinImeLogger.sVISUALDEBUG) {
@@ -747,7 +691,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
}
}
- private static int getCharGeometryCacheKey(char referenceChar, Paint paint) {
+ private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) {
final int labelSize = (int)paint.getTextSize();
final Typeface face = paint.getTypeface();
final int codePointOffset = referenceChar << 15;
@@ -765,8 +709,8 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
// Working variable for the following methods.
private final Rect mTextBounds = new Rect();
- private float getCharHeight(char[] referenceChar, Paint paint) {
- final Integer key = getCharGeometryCacheKey(referenceChar[0], paint);
+ private float getCharHeight(final char[] referenceChar, final Paint paint) {
+ final int key = getCharGeometryCacheKey(referenceChar[0], paint);
final Float cachedValue = sTextHeightCache.get(key);
if (cachedValue != null)
return cachedValue;
@@ -777,8 +721,8 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
return height;
}
- private float getCharWidth(char[] referenceChar, Paint paint) {
- final Integer key = getCharGeometryCacheKey(referenceChar[0], paint);
+ private float getCharWidth(final char[] referenceChar, final Paint paint) {
+ final int key = getCharGeometryCacheKey(referenceChar[0], paint);
final Float cachedValue = sTextWidthCache.get(key);
if (cachedValue != null)
return cachedValue;
@@ -789,36 +733,38 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
return width;
}
- public float getLabelWidth(String label, Paint paint) {
- paint.getTextBounds(label.toString(), 0, label.length(), mTextBounds);
+ // TODO: Remove this method.
+ public float getLabelWidth(final String label, final Paint paint) {
+ paint.getTextBounds(label, 0, label.length(), mTextBounds);
return mTextBounds.width();
}
- protected static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width,
- int height) {
+ protected static void drawIcon(final Canvas canvas, final Drawable icon, final int x,
+ final int y, final int width, final int height) {
canvas.translate(x, y);
icon.setBounds(0, 0, width, height);
icon.draw(canvas);
canvas.translate(-x, -y);
}
- private static void drawHorizontalLine(Canvas canvas, float y, float w, int color,
- Paint paint) {
+ private static void drawHorizontalLine(final Canvas canvas, final float y, final float w,
+ final int color, final Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1.0f);
paint.setColor(color);
canvas.drawLine(0, y, w, y, paint);
}
- private static void drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint) {
+ private static void drawVerticalLine(final Canvas canvas, final float x, final float h,
+ final int color, final Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1.0f);
paint.setColor(color);
canvas.drawLine(x, 0, x, h, paint);
}
- private static void drawRectangle(Canvas canvas, float x, float y, float w, float h, int color,
- Paint paint) {
+ private static void drawRectangle(final Canvas canvas, final float x, final float y,
+ final float w, final float h, final int color, final Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(1.0f);
paint.setColor(color);
@@ -827,18 +773,11 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
canvas.translate(-x, -y);
}
- // Overlay a dark rectangle to dim.
- private static void drawDimRectangle(Canvas canvas, Rect rect, int alpha, Paint paint) {
- paint.setColor(Color.BLACK);
- paint.setAlpha(alpha);
- canvas.drawRect(rect, paint);
- }
-
public Paint newDefaultLabelPaint() {
final Paint paint = new Paint();
paint.setAntiAlias(true);
- paint.setTypeface(mKeyDrawParams.mKeyTextStyle);
- paint.setTextSize(mKeyDrawParams.mKeyLabelSize);
+ paint.setTypeface(mKeyDrawParams.mTypeface);
+ paint.setTextSize(mKeyDrawParams.mLabelSize);
return paint;
}
@@ -846,38 +785,101 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
mDrawingHandler.cancelAllMessages();
}
- // Called by {@link PointerTracker} constructor to create a TextView.
- @Override
- public TextView inflateKeyPreviewText() {
+ private TextView getKeyPreviewText(final int pointerId) {
+ TextView previewText = mKeyPreviewTexts.get(pointerId);
+ if (previewText != null) {
+ return previewText;
+ }
final Context context = getContext();
if (mKeyPreviewLayoutId != 0) {
- return (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
+ previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
} else {
- return new TextView(context);
+ previewText = new TextView(context);
}
+ mKeyPreviewTexts.put(pointerId, previewText);
+ return previewText;
+ }
+
+ private void dismissAllKeyPreviews() {
+ final int pointerCount = mKeyPreviewTexts.size();
+ for (int id = 0; id < pointerCount; id++) {
+ final TextView previewText = mKeyPreviewTexts.get(id);
+ if (previewText != null) {
+ previewText.setVisibility(INVISIBLE);
+ }
+ }
+ PointerTracker.setReleasedKeyGraphicsToAllKeys();
}
@Override
- public void dismissKeyPreview(PointerTracker tracker) {
+ public void dismissKeyPreview(final PointerTracker tracker) {
mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
}
- private void addKeyPreview(TextView keyPreview) {
- if (mPreviewPlacer == null) {
- mPreviewPlacer = new RelativeLayout(getContext());
- final ViewGroup windowContentView =
- (ViewGroup)getRootView().findViewById(android.R.id.content);
- windowContentView.addView(mPreviewPlacer);
+ private void addKeyPreview(final TextView keyPreview) {
+ locatePreviewPlacerView();
+ mPreviewPlacerView.addView(
+ keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0));
+ }
+
+ private void locatePreviewPlacerView() {
+ if (mPreviewPlacerView.getParent() != null) {
+ return;
}
- mPreviewPlacer.addView(
- keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacer, 0, 0));
+ final int width = getWidth();
+ final int height = getHeight();
+ if (width == 0 || height == 0) {
+ // In transient state.
+ return;
+ }
+ final int[] viewOrigin = new int[2];
+ getLocationInWindow(viewOrigin);
+ final DisplayMetrics dm = getResources().getDisplayMetrics();
+ if (viewOrigin[1] < dm.heightPixels / 4) {
+ // In transient state.
+ return;
+ }
+ final View rootView = getRootView();
+ if (rootView == null) {
+ Log.w(TAG, "Cannot find root view");
+ return;
+ }
+ final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content);
+ // Note: It'd be very weird if we get null by android.R.id.content.
+ if (windowContentView == null) {
+ Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView");
+ } else {
+ windowContentView.addView(mPreviewPlacerView);
+ mPreviewPlacerView.setKeyboardViewGeometry(viewOrigin[0], viewOrigin[1], width, height);
+ }
+ }
+
+ public void showGestureFloatingPreviewText(final String gestureFloatingPreviewText) {
+ locatePreviewPlacerView();
+ mPreviewPlacerView.setGestureFloatingPreviewText(gestureFloatingPreviewText);
+ }
+
+ public void dismissGestureFloatingPreviewText() {
+ locatePreviewPlacerView();
+ mPreviewPlacerView.dismissGestureFloatingPreviewText();
+ }
+
+ @Override
+ public void showGesturePreviewTrail(final PointerTracker tracker,
+ final boolean isOldestTracker) {
+ locatePreviewPlacerView();
+ mPreviewPlacerView.invalidatePointer(tracker, isOldestTracker);
}
@Override
- public void showKeyPreview(PointerTracker tracker) {
- if (!mShowKeyPreviewPopup) return;
+ public void showKeyPreview(final PointerTracker tracker) {
+ final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams;
+ if (!mShowKeyPreviewPopup) {
+ previewParams.mPreviewVisibleOffset = -mKeyboard.mVerticalGap;
+ return;
+ }
- final TextView previewText = tracker.getKeyPreviewText();
+ final TextView previewText = getKeyPreviewText(tracker.mPointerId);
// If the key preview has no parent view yet, add it to the ViewGroup which can place
// key preview absolutely in SoftInputWindow.
if (previewText.getParent() == null) {
@@ -889,21 +891,28 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
// If key is invalid or IME is already closed, we must not show key preview.
// Trying to show key preview while root window is closed causes
// WindowManager.BadTokenException.
- if (key == null)
+ if (key == null) {
return;
+ }
- final KeyPreviewDrawParams params = mKeyPreviewDrawParams;
+ final KeyDrawParams drawParams = mKeyDrawParams;
+ previewText.setTextColor(drawParams.mPreviewTextColor);
+ final Drawable background = previewText.getBackground();
+ if (background != null) {
+ background.setState(KEY_PREVIEW_BACKGROUND_DEFAULT_STATE);
+ background.setAlpha(PREVIEW_ALPHA);
+ }
final String label = key.isShiftedLetterActivated() ? key.mHintLabel : key.mLabel;
- // What we show as preview should match what we show on a key top in onBufferDraw().
+ // What we show as preview should match what we show on a key top in onDraw().
if (label != null) {
// TODO Should take care of temporaryShiftLabel here.
previewText.setCompoundDrawables(null, null, null, null);
if (StringUtils.codePointCount(label) > 1) {
- previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mKeyLetterSize);
+ previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mLetterSize);
previewText.setTypeface(Typeface.DEFAULT_BOLD);
} else {
- previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mPreviewTextSize);
- previewText.setTypeface(params.mKeyTextStyle);
+ previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mPreviewTextSize);
+ previewText.setTypeface(key.selectTypeface(drawParams));
}
previewText.setText(label);
} else {
@@ -911,48 +920,44 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
key.getPreviewIcon(mKeyboard.mIconsSet));
previewText.setText(null);
}
- previewText.setBackgroundDrawable(params.mPreviewBackground);
previewText.measure(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
+ final int keyDrawWidth = key.getDrawWidth();
final int previewWidth = previewText.getMeasuredWidth();
- final int previewHeight = params.mPreviewHeight;
+ final int previewHeight = mPreviewHeight;
// The width and height of visible part of the key preview background. The content marker
// of the background 9-patch have to cover the visible part of the background.
- params.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft()
+ previewParams.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft()
- previewText.getPaddingRight();
- params.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop()
+ previewParams.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop()
- previewText.getPaddingBottom();
// The distance between the top edge of the parent key and the bottom of the visible part
// of the key preview background.
- params.mPreviewVisibleOffset = params.mPreviewOffset - previewText.getPaddingBottom();
- getLocationInWindow(params.mCoordinates);
+ previewParams.mPreviewVisibleOffset = mPreviewOffset - previewText.getPaddingBottom();
+ getLocationInWindow(mCoordinates);
// The key preview is horizontally aligned with the center of the visible part of the
// parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and
// the left/right background is used if such background is specified.
- int previewX = key.mX + key.mVisualInsetsLeft - (previewWidth - keyDrawWidth) / 2
- + params.mCoordinates[0];
+ final int statePosition;
+ int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 + mCoordinates[0];
if (previewX < 0) {
previewX = 0;
- if (params.mPreviewLeftBackground != null) {
- previewText.setBackgroundDrawable(params.mPreviewLeftBackground);
- }
+ statePosition = STATE_LEFT;
} else if (previewX > getWidth() - previewWidth) {
previewX = getWidth() - previewWidth;
- if (params.mPreviewRightBackground != null) {
- previewText.setBackgroundDrawable(params.mPreviewRightBackground);
- }
+ statePosition = STATE_RIGHT;
+ } else {
+ statePosition = STATE_MIDDLE;
}
// The key preview is placed vertically above the top edge of the parent key with an
// arbitrary offset.
- final int previewY = key.mY - previewHeight + params.mPreviewOffset
- + params.mCoordinates[1];
+ final int previewY = key.mY - previewHeight + mPreviewOffset + mCoordinates[1];
- // Set the preview background state
- previewText.getBackground().setState(
- key.mMoreKeys != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
- previewText.setTextColor(params.mPreviewTextColor);
+ if (background != null) {
+ final int hasMoreKeys = (key.mMoreKeys != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL;
+ background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]);
+ }
ViewLayoutUtils.placeViewAt(
previewText, previewX, previewY, previewWidth, previewHeight);
previewText.setVisibility(VISIBLE);
@@ -967,7 +972,6 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
public void invalidateAllKeys() {
mInvalidatedKeys.clear();
mInvalidateAllKeys = true;
- mBufferNeedsUpdate = true;
invalidate();
}
@@ -979,19 +983,17 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
* @see #invalidateAllKeys
*/
@Override
- public void invalidateKey(Key key) {
+ public void invalidateKey(final Key key) {
if (mInvalidateAllKeys) return;
if (key == null) return;
mInvalidatedKeys.add(key);
final int x = key.mX + getPaddingLeft();
final int y = key.mY + getPaddingTop();
- mInvalidatedKeysRect.union(x, y, x + key.mWidth, y + key.mHeight);
- mBufferNeedsUpdate = true;
- invalidate(mInvalidatedKeysRect);
+ invalidate(x, y, x + key.mWidth, y + key.mHeight);
}
public void closing() {
- PointerTracker.dismissAllKeyPreviews();
+ dismissAllKeyPreviews();
cancelAllMessages();
mInvalidateAllKeys = true;
@@ -1012,12 +1014,7 @@ public class KeyboardView extends View implements PointerTracker.DrawingProxy {
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
closing();
- if (mPreviewPlacer != null) {
- mPreviewPlacer.removeAllViews();
- }
- if (mBuffer != null) {
- mBuffer.recycle();
- mBuffer = null;
- }
+ mPreviewPlacerView.removeAllViews();
+ freeOffscreenBuffer();
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 383298de9..3e6f92c2a 100644
--- a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -43,16 +43,19 @@ import com.android.inputmethod.accessibility.AccessibilityUtils;
import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
+import com.android.inputmethod.keyboard.internal.KeyDrawParams;
+import com.android.inputmethod.keyboard.internal.SuddenJumpingTouchEventHandler;
+import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResearchLogger;
+import com.android.inputmethod.latin.ResourceUtils;
import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
import com.android.inputmethod.latin.StringUtils;
import com.android.inputmethod.latin.SubtypeLocale;
-import com.android.inputmethod.latin.Utils;
import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
import java.util.Locale;
import java.util.WeakHashMap;
@@ -60,13 +63,40 @@ import java.util.WeakHashMap;
/**
* A view that is responsible for detecting key presses and touch movements.
*
- * @attr ref R.styleable#KeyboardView_keyHysteresisDistance
- * @attr ref R.styleable#KeyboardView_verticalCorrection
- * @attr ref R.styleable#KeyboardView_popupLayout
+ * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedEnabled
+ * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedIcon
+ * @attr ref R.styleable#MainKeyboardView_spacebarTextRatio
+ * @attr ref R.styleable#MainKeyboardView_spacebarTextColor
+ * @attr ref R.styleable#MainKeyboardView_spacebarTextShadowColor
+ * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFinalAlpha
+ * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFadeoutAnimator
+ * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator
+ * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator
+ * @attr ref R.styleable#MainKeyboardView_keyHysteresisDistance
+ * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdTime
+ * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdDistance
+ * @attr ref R.styleable#MainKeyboardView_slidingKeyInputEnable
+ * @attr ref R.styleable#MainKeyboardView_keyRepeatStartTimeout
+ * @attr ref R.styleable#MainKeyboardView_keyRepeatInterval
+ * @attr ref R.styleable#MainKeyboardView_longPressKeyTimeout
+ * @attr ref R.styleable#MainKeyboardView_longPressShiftKeyTimeout
+ * @attr ref R.styleable#MainKeyboardView_ignoreAltCodeKeyTimeout
+ * @attr ref R.styleable#MainKeyboardView_showMoreKeysKeyboardAtTouchPoint
+ * @attr ref R.styleable#MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping
+ * @attr ref R.styleable#MainKeyboardView_gestureDetectFastMoveSpeedThreshold
+ * @attr ref R.styleable#MainKeyboardView_gestureDynamicThresholdDecayDuration
+ * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdFrom
+ * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdTo
+ * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdFrom
+ * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdTo
+ * @attr ref R.styleable#MainKeyboardView_gestureSamplingMinimumDistance
+ * @attr ref R.styleable#MainKeyboardView_gestureRecognitionMinimumTime
+ * @attr ref R.styleable#MainKeyboardView_gestureRecognitionSpeedThreshold
+ * @attr ref R.styleable#MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration
*/
-public class LatinKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
+public final class MainKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
SuddenJumpingTouchEventHandler.ProcessMotionEvent {
- private static final String TAG = LatinKeyboardView.class.getSimpleName();
+ 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;
@@ -80,10 +110,9 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
// Stuff to draw language name on spacebar.
private final int mLanguageOnSpacebarFinalAlpha;
private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator;
- private static final int ALPHA_OPAQUE = 255;
private boolean mNeedsToDisplayLanguage;
private boolean mHasMultipleEnabledIMEsOrSubtypes;
- private int mLanguageOnSpacebarAnimAlpha = ALPHA_OPAQUE;
+ private int mLanguageOnSpacebarAnimAlpha = Constants.Color.ALPHA_OPAQUE;
private final float mSpacebarTextRatio;
private float mSpacebarTextSize;
private final int mSpacebarTextColor;
@@ -99,7 +128,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
// Stuff to draw altCodeWhileTyping keys.
private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
- private int mAltCodeKeyWhileTypingAnimAlpha = ALPHA_OPAQUE;
+ private int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE;
// More keys keyboard
private PopupWindow mMoreKeysWindow;
@@ -109,7 +138,6 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
new WeakHashMap<Key, MoreKeysPanel>();
private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint;
- private final PointerTrackerParams mPointerTrackerParams;
private final SuddenJumpingTouchEventHandler mTouchScreenRegulator;
protected KeyDetector mKeyDetector;
@@ -119,29 +147,49 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
private final KeyTimerHandler mKeyTimerHandler;
- private static class KeyTimerHandler extends StaticInnerHandlerWrapper<LatinKeyboardView>
+ private static final class KeyTimerHandler extends StaticInnerHandlerWrapper<MainKeyboardView>
implements TimerProxy {
+ private static final int MSG_TYPING_STATE_EXPIRED = 0;
private static final int MSG_REPEAT_KEY = 1;
private static final int MSG_LONGPRESS_KEY = 2;
private static final int MSG_DOUBLE_TAP = 3;
- private static final int MSG_TYPING_STATE_EXPIRED = 4;
- private final KeyTimerParams mParams;
- private boolean mInKeyRepeat;
+ private final int mKeyRepeatStartTimeout;
+ private final int mKeyRepeatInterval;
+ private final int mLongPressKeyTimeout;
+ private final int mLongPressShiftKeyTimeout;
+ private final int mIgnoreAltCodeKeyTimeout;
- public KeyTimerHandler(LatinKeyboardView outerInstance, KeyTimerParams params) {
+ public KeyTimerHandler(final MainKeyboardView outerInstance,
+ final TypedArray mainKeyboardViewAttr) {
super(outerInstance);
- mParams = params;
+
+ mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0);
+ mKeyRepeatInterval = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_keyRepeatInterval, 0);
+ mLongPressKeyTimeout = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_longPressKeyTimeout, 0);
+ mLongPressShiftKeyTimeout = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_longPressShiftKeyTimeout, 0);
+ mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0);
}
@Override
- public void handleMessage(Message msg) {
- final LatinKeyboardView keyboardView = getOuterInstance();
+ public void handleMessage(final Message msg) {
+ final MainKeyboardView keyboardView = getOuterInstance();
final PointerTracker tracker = (PointerTracker) msg.obj;
switch (msg.what) {
+ case MSG_TYPING_STATE_EXPIRED:
+ startWhileTypingFadeinAnimation(keyboardView);
+ break;
case MSG_REPEAT_KEY:
- tracker.onRegisterKey(tracker.getKey());
- startKeyRepeatTimer(tracker, mParams.mKeyRepeatInterval);
+ final Key currentKey = tracker.getKey();
+ if (currentKey != null && currentKey.mCode == msg.arg1) {
+ tracker.onRegisterKey(currentKey);
+ startKeyRepeatTimer(tracker, mKeyRepeatInterval);
+ }
break;
case MSG_LONGPRESS_KEY:
if (tracker != null) {
@@ -150,39 +198,36 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
KeyboardSwitcher.getInstance().onLongPressTimeout(msg.arg1);
}
break;
- case MSG_TYPING_STATE_EXPIRED:
- cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator,
- keyboardView.mAltCodeKeyWhileTypingFadeinAnimator);
- break;
}
}
- private void startKeyRepeatTimer(PointerTracker tracker, long delay) {
- sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, tracker), delay);
+ private void startKeyRepeatTimer(final PointerTracker tracker, final long delay) {
+ final Key key = tracker.getKey();
+ if (key == null) return;
+ sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, key.mCode, 0, tracker), delay);
}
@Override
- public void startKeyRepeatTimer(PointerTracker tracker) {
- mInKeyRepeat = true;
- startKeyRepeatTimer(tracker, mParams.mKeyRepeatStartTimeout);
+ public void startKeyRepeatTimer(final PointerTracker tracker) {
+ startKeyRepeatTimer(tracker, mKeyRepeatStartTimeout);
}
public void cancelKeyRepeatTimer() {
- mInKeyRepeat = false;
removeMessages(MSG_REPEAT_KEY);
}
+ // TODO: Suppress layout changes in key repeat mode
public boolean isInKeyRepeat() {
- return mInKeyRepeat;
+ return hasMessages(MSG_REPEAT_KEY);
}
@Override
- public void startLongPressTimer(int code) {
+ public void startLongPressTimer(final int code) {
cancelLongPressTimer();
final int delay;
switch (code) {
case Keyboard.CODE_SHIFT:
- delay = mParams.mLongPressShiftKeyTimeout;
+ delay = mLongPressShiftKeyTimeout;
break;
default:
delay = 0;
@@ -194,7 +239,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
@Override
- public void startLongPressTimer(PointerTracker tracker) {
+ public void startLongPressTimer(final PointerTracker tracker) {
cancelLongPressTimer();
if (tracker == null) {
return;
@@ -203,15 +248,15 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
final int delay;
switch (key.mCode) {
case Keyboard.CODE_SHIFT:
- delay = mParams.mLongPressShiftKeyTimeout;
+ delay = mLongPressShiftKeyTimeout;
break;
default:
if (KeyboardSwitcher.getInstance().isInMomentarySwitchState()) {
// We use longer timeout for sliding finger input started from the symbols
// mode key.
- delay = mParams.mLongPressKeyTimeout * 3;
+ delay = mLongPressKeyTimeout * 3;
} else {
- delay = mParams.mLongPressKeyTimeout;
+ delay = mLongPressKeyTimeout;
}
break;
}
@@ -225,7 +270,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
removeMessages(MSG_LONGPRESS_KEY);
}
- public static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
+ private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
final ObjectAnimator animatorToStart) {
float startFraction = 0.0f;
if (animatorToCancel.isStarted()) {
@@ -237,18 +282,39 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
animatorToStart.setCurrentPlayTime(startTime);
}
+ private static void startWhileTypingFadeinAnimation(final MainKeyboardView keyboardView) {
+ cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator,
+ keyboardView.mAltCodeKeyWhileTypingFadeinAnimator);
+ }
+
+ private static void startWhileTypingFadeoutAnimation(final MainKeyboardView keyboardView) {
+ cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator,
+ keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator);
+ }
+
@Override
- public void startTypingStateTimer() {
+ public void startTypingStateTimer(final Key typedKey) {
+ if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) {
+ return;
+ }
+
final boolean isTyping = isTypingState();
removeMessages(MSG_TYPING_STATE_EXPIRED);
+ final MainKeyboardView keyboardView = getOuterInstance();
+
+ // When user hits the space or the enter key, just cancel the while-typing timer.
+ final int typedCode = typedKey.mCode;
+ if (typedCode == Keyboard.CODE_SPACE || typedCode == Keyboard.CODE_ENTER) {
+ startWhileTypingFadeinAnimation(keyboardView);
+ return;
+ }
+
sendMessageDelayed(
- obtainMessage(MSG_TYPING_STATE_EXPIRED), mParams.mIgnoreAltCodeKeyTimeout);
+ obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout);
if (isTyping) {
return;
}
- final LatinKeyboardView keyboardView = getOuterInstance();
- cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator,
- keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator);
+ startWhileTypingFadeoutAnimation(keyboardView);
}
@Override
@@ -283,99 +349,56 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
}
- public static class PointerTrackerParams {
- public final boolean mSlidingKeyInputEnabled;
- public final int mTouchNoiseThresholdTime;
- public final float mTouchNoiseThresholdDistance;
-
- public static final PointerTrackerParams DEFAULT = new PointerTrackerParams();
-
- private PointerTrackerParams() {
- mSlidingKeyInputEnabled = false;
- mTouchNoiseThresholdTime =0;
- mTouchNoiseThresholdDistance = 0;
- }
-
- public PointerTrackerParams(TypedArray latinKeyboardViewAttr) {
- mSlidingKeyInputEnabled = latinKeyboardViewAttr.getBoolean(
- R.styleable.LatinKeyboardView_slidingKeyInputEnable, false);
- mTouchNoiseThresholdTime = latinKeyboardViewAttr.getInt(
- R.styleable.LatinKeyboardView_touchNoiseThresholdTime, 0);
- mTouchNoiseThresholdDistance = latinKeyboardViewAttr.getDimension(
- R.styleable.LatinKeyboardView_touchNoiseThresholdDistance, 0);
- }
+ public MainKeyboardView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, R.attr.mainKeyboardViewStyle);
}
- static class KeyTimerParams {
- public final int mKeyRepeatStartTimeout;
- public final int mKeyRepeatInterval;
- public final int mLongPressKeyTimeout;
- public final int mLongPressShiftKeyTimeout;
- public final int mIgnoreAltCodeKeyTimeout;
-
- public KeyTimerParams(TypedArray latinKeyboardViewAttr) {
- mKeyRepeatStartTimeout = latinKeyboardViewAttr.getInt(
- R.styleable.LatinKeyboardView_keyRepeatStartTimeout, 0);
- mKeyRepeatInterval = latinKeyboardViewAttr.getInt(
- R.styleable.LatinKeyboardView_keyRepeatInterval, 0);
- mLongPressKeyTimeout = latinKeyboardViewAttr.getInt(
- R.styleable.LatinKeyboardView_longPressKeyTimeout, 0);
- mLongPressShiftKeyTimeout = latinKeyboardViewAttr.getInt(
- R.styleable.LatinKeyboardView_longPressShiftKeyTimeout, 0);
- mIgnoreAltCodeKeyTimeout = latinKeyboardViewAttr.getInt(
- R.styleable.LatinKeyboardView_ignoreAltCodeKeyTimeout, 0);
- }
- }
-
- public LatinKeyboardView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.latinKeyboardViewStyle);
- }
-
- public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
+ public MainKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
mTouchScreenRegulator = new SuddenJumpingTouchEventHandler(getContext(), this);
mHasDistinctMultitouch = context.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
+ final Resources res = getResources();
final boolean needsPhantomSuddenMoveEventHack = Boolean.parseBoolean(
- Utils.getDeviceOverrideValue(context.getResources(),
+ ResourceUtils.getDeviceOverrideValue(res,
R.array.phantom_sudden_move_event_device_list, "false"));
PointerTracker.init(mHasDistinctMultitouch, needsPhantomSuddenMoveEventHack);
final TypedArray a = context.obtainStyledAttributes(
- attrs, R.styleable.LatinKeyboardView, defStyle, R.style.LatinKeyboardView);
+ attrs, R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView);
mAutoCorrectionSpacebarLedEnabled = a.getBoolean(
- R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedEnabled, false);
+ R.styleable.MainKeyboardView_autoCorrectionSpacebarLedEnabled, false);
mAutoCorrectionSpacebarLedIcon = a.getDrawable(
- R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedIcon);
- mSpacebarTextRatio = a.getFraction(R.styleable.LatinKeyboardView_spacebarTextRatio,
- 1000, 1000, 1) / 1000.0f;
- mSpacebarTextColor = a.getColor(R.styleable.LatinKeyboardView_spacebarTextColor, 0);
+ R.styleable.MainKeyboardView_autoCorrectionSpacebarLedIcon);
+ mSpacebarTextRatio = a.getFraction(
+ R.styleable.MainKeyboardView_spacebarTextRatio, 1, 1, 1.0f);
+ mSpacebarTextColor = a.getColor(R.styleable.MainKeyboardView_spacebarTextColor, 0);
mSpacebarTextShadowColor = a.getColor(
- R.styleable.LatinKeyboardView_spacebarTextShadowColor, 0);
+ R.styleable.MainKeyboardView_spacebarTextShadowColor, 0);
mLanguageOnSpacebarFinalAlpha = a.getInt(
- R.styleable.LatinKeyboardView_languageOnSpacebarFinalAlpha, ALPHA_OPAQUE);
+ R.styleable.MainKeyboardView_languageOnSpacebarFinalAlpha,
+ Constants.Color.ALPHA_OPAQUE);
final int languageOnSpacebarFadeoutAnimatorResId = a.getResourceId(
- R.styleable.LatinKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
+ R.styleable.MainKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
final int altCodeKeyWhileTypingFadeoutAnimatorResId = a.getResourceId(
- R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
+ R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
final int altCodeKeyWhileTypingFadeinAnimatorResId = a.getResourceId(
- R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
-
- final KeyTimerParams keyTimerParams = new KeyTimerParams(a);
- mPointerTrackerParams = new PointerTrackerParams(a);
+ R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
final float keyHysteresisDistance = a.getDimension(
- R.styleable.LatinKeyboardView_keyHysteresisDistance, 0);
- mKeyDetector = new KeyDetector(keyHysteresisDistance);
- mKeyTimerHandler = new KeyTimerHandler(this, keyTimerParams);
+ R.styleable.MainKeyboardView_keyHysteresisDistance, 0);
+ final float keyHysteresisDistanceForSlidingModifier = a.getDimension(
+ R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0);
+ mKeyDetector = new KeyDetector(
+ keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier);
+ mKeyTimerHandler = new KeyTimerHandler(this, a);
mConfigShowMoreKeysKeyboardAtTouchedPoint = a.getBoolean(
- R.styleable.LatinKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
+ R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
+ PointerTracker.setParameters(a);
a.recycle();
- PointerTracker.setParameters(mPointerTrackerParams);
-
mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator(
languageOnSpacebarFadeoutAnimatorResId, this);
mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator(
@@ -384,7 +407,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
altCodeKeyWhileTypingFadeinAnimatorResId, this);
}
- private ObjectAnimator loadObjectAnimator(int resId, Object target) {
+ private ObjectAnimator loadObjectAnimator(final int resId, final Object target) {
if (resId == 0) return null;
final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator(
getContext(), resId);
@@ -399,7 +422,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
return mLanguageOnSpacebarAnimAlpha;
}
- public void setLanguageOnSpacebarAnimAlpha(int alpha) {
+ public void setLanguageOnSpacebarAnimAlpha(final int alpha) {
mLanguageOnSpacebarAnimAlpha = alpha;
invalidateKey(mSpaceKey);
}
@@ -408,12 +431,12 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
return mAltCodeKeyWhileTypingAnimAlpha;
}
- public void setAltCodeKeyWhileTypingAnimAlpha(int alpha) {
+ public void setAltCodeKeyWhileTypingAnimAlpha(final int alpha) {
mAltCodeKeyWhileTypingAnimAlpha = alpha;
updateAltCodeKeyWhileTyping();
}
- public void setKeyboardActionListener(KeyboardActionListener listener) {
+ public void setKeyboardActionListener(final KeyboardActionListener listener) {
mKeyboardActionListener = listener;
PointerTracker.setKeyboardActionListener(listener);
}
@@ -450,9 +473,9 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
* @param keyboard the keyboard to display in this view
*/
@Override
- public void setKeyboard(Keyboard keyboard) {
- // Remove any pending messages, except dismissing preview
- mKeyTimerHandler.cancelKeyTimers();
+ public void setKeyboard(final Keyboard keyboard) {
+ // Remove any pending messages, except dismissing preview and key repeat.
+ mKeyTimerHandler.cancelLongPressTimer();
super.setKeyboard(keyboard);
mKeyDetector.setKeyboard(
keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection);
@@ -462,11 +485,11 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
mSpaceKey = keyboard.getKey(Keyboard.CODE_SPACE);
mSpaceIcon = (mSpaceKey != null)
- ? mSpaceKey.getIcon(keyboard.mIconsSet, ALPHA_OPAQUE) : null;
+ ? mSpaceKey.getIcon(keyboard.mIconsSet, Constants.Color.ALPHA_OPAQUE) : null;
final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
mSpacebarTextSize = keyHeight * mSpacebarTextRatio;
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinKeyboardView_setKeyboard(keyboard);
+ ResearchLogger.mainKeyboardView_setKeyboard(keyboard);
}
// This always needs to be set since the accessibility state can
@@ -474,6 +497,15 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
AccessibleKeyboardViewProxy.getInstance().setKeyboard(keyboard);
}
+ // Note that this method is called from a non-UI thread.
+ public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) {
+ PointerTracker.setMainDictionaryAvailability(mainDictionaryAvailable);
+ }
+
+ public void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) {
+ PointerTracker.setGestureHandlingEnabledByUser(gestureHandlingEnabledByUser);
+ }
+
/**
* Returns whether the device has distinct multi-touch panel.
* @return true if the device has distinct multi-touch panel.
@@ -482,25 +514,29 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
return mHasDistinctMultitouch;
}
- public void setDistinctMultitouch(boolean hasDistinctMultitouch) {
+ public void setDistinctMultitouch(final boolean hasDistinctMultitouch) {
mHasDistinctMultitouch = hasDistinctMultitouch;
}
- /**
- * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
- * codes for adjacent keys. When disabled, only the primary key code will be
- * reported.
- * @param enabled whether or not the proximity correction is enabled
- */
- public void setProximityCorrectionEnabled(boolean enabled) {
- mKeyDetector.setProximityCorrectionEnabled(enabled);
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ // Notify the research logger that the keyboard view has been attached. This is needed
+ // to properly show the splash screen, which requires that the window token of the
+ // KeyboardView be non-null.
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().mainKeyboardView_onAttachedToWindow(this);
+ }
}
- /**
- * Returns true if proximity correction is enabled.
- */
- public boolean isProximityCorrectionEnabled() {
- return mKeyDetector.isProximityCorrectionEnabled();
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ // Notify the research logger that the keyboard view has been detached. This is needed
+ // to invalidate the reference of {@link MainKeyboardView} to null.
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().mainKeyboardView_onDetachedFromWindow();
+ }
}
@Override
@@ -509,7 +545,8 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
super.cancelAllMessages();
}
- private boolean openMoreKeysKeyboardIfRequired(Key parentKey, PointerTracker tracker) {
+ private boolean openMoreKeysKeyboardIfRequired(final Key parentKey,
+ final PointerTracker tracker) {
// Check if we have a popup layout specified first.
if (mMoreKeysLayout == 0) {
return false;
@@ -524,7 +561,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
// This default implementation returns a more keys panel.
- protected MoreKeysPanel onCreateMoreKeysPanel(Key parentKey) {
+ protected MoreKeysPanel onCreateMoreKeysPanel(final Key parentKey) {
if (parentKey.mMoreKeys == null)
return null;
@@ -550,9 +587,9 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
* @return true if the long press is handled, false otherwise. Subclasses should call the
* method on the base class if the subclass doesn't wish to handle the call.
*/
- protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
+ protected boolean onLongPress(final Key parentKey, final PointerTracker tracker) {
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinKeyboardView_onLongPress();
+ ResearchLogger.mainKeyboardView_onLongPress();
}
final int primaryCode = parentKey.mCode;
if (parentKey.hasEmbeddedMoreKey()) {
@@ -574,21 +611,20 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
return openMoreKeysPanel(parentKey, tracker);
}
- private boolean invokeCustomRequest(int code) {
+ private boolean invokeCustomRequest(final int code) {
return mKeyboardActionListener.onCustomRequest(code);
}
- private void invokeCodeInput(int primaryCode) {
- mKeyboardActionListener.onCodeInput(primaryCode,
- KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
- KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
+ private void invokeCodeInput(final int primaryCode) {
+ mKeyboardActionListener.onCodeInput(
+ primaryCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
}
- private void invokeReleaseKey(int primaryCode) {
+ private void invokeReleaseKey(final int primaryCode) {
mKeyboardActionListener.onReleaseKey(primaryCode, false);
}
- private boolean openMoreKeysPanel(Key parentKey, PointerTracker tracker) {
+ private boolean openMoreKeysPanel(final Key parentKey, final PointerTracker tracker) {
MoreKeysPanel moreKeysPanel = mMoreKeysPanelCache.get(parentKey);
if (moreKeysPanel == null) {
moreKeysPanel = onCreateMoreKeysPanel(parentKey);
@@ -614,9 +650,9 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
// 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.
- final int pointY = parentKey.mY + (keyPreviewEnabled
- ? mKeyPreviewDrawParams.mPreviewVisibleOffset
- : -parentKey.mVerticalGap);
+ // {@code mPreviewVisibleOffset} has been set appropriately in
+ // {@link KeyboardView#showKeyPreview(PointerTracker)}.
+ final int pointY = parentKey.mY + mKeyPreviewDrawParams.mPreviewVisibleOffset;
moreKeysPanel.showMoreKeysPanel(
this, this, pointX, pointY, mMoreKeysWindow, mKeyboardActionListener);
final int translatedX = moreKeysPanel.translateX(tracker.getLastX());
@@ -639,7 +675,15 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
@Override
- public boolean onTouchEvent(MotionEvent me) {
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
+ return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event);
+ }
+ return super.dispatchTouchEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(final MotionEvent me) {
if (getKeyboard() == null) {
return false;
}
@@ -647,7 +691,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
@Override
- public boolean processMotionEvent(MotionEvent me) {
+ public boolean processMotionEvent(final MotionEvent me) {
final boolean nonDistinctMultitouch = !mHasDistinctMultitouch;
final int action = me.getActionMasked();
final int pointerCount = me.getPointerCount();
@@ -703,7 +747,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
}
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime, index, id,
+ ResearchLogger.mainKeyboardView_processMotionEvent(me, action, eventTime, index, id,
x, y);
}
@@ -755,15 +799,18 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
final PointerTracker tracker = PointerTracker.getPointerTracker(
pointerId, this);
final int px, py;
+ final MotionEvent motionEvent;
if (mMoreKeysPanel != null
&& tracker.mPointerId == mMoreKeysPanelPointerTrackerId) {
px = mMoreKeysPanel.translateX((int)me.getX(i));
py = mMoreKeysPanel.translateY((int)me.getY(i));
+ motionEvent = null;
} else {
px = (int)me.getX(i);
py = (int)me.getY(i);
+ motionEvent = me;
}
- tracker.onMoveEvent(px, py, eventTime);
+ tracker.onMoveEvent(px, py, eventTime, motionEvent);
if (ENABLE_USABILITY_STUDY_LOG) {
final float pointerSize = me.getSize(i);
final float pointerPressure = me.getPressure(i);
@@ -772,7 +819,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
+ pointerSize + "," + pointerPressure);
}
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime,
+ ResearchLogger.mainKeyboardView_processMotionEvent(me, action, eventTime,
i, pointerId, px, py);
}
}
@@ -803,20 +850,6 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
return false;
}
- @Override
- public void draw(Canvas c) {
- Utils.GCUtils.getInstance().reset();
- boolean tryGC = true;
- for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
- try {
- super.draw(c);
- tryGC = false;
- } catch (OutOfMemoryError e) {
- tryGC = Utils.GCUtils.getInstance().tryGCOrWait(TAG, e);
- }
- }
- }
-
/**
* Receives hover events from the input framework.
*
@@ -825,7 +858,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
* otherwise
*/
@Override
- public boolean dispatchHoverEvent(MotionEvent event) {
+ public boolean dispatchHoverEvent(final MotionEvent event) {
if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker);
@@ -835,7 +868,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
return false;
}
- public void updateShortcutKey(boolean available) {
+ public void updateShortcutKey(final boolean available) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) return;
final Key shortcutKey = keyboard.getKey(Keyboard.CODE_SHORTCUT);
@@ -852,8 +885,8 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
}
- public void startDisplayLanguageOnSpacebar(boolean subtypeChanged,
- boolean needsToDisplayLanguage, boolean hasMultipleEnabledIMEsOrSubtypes) {
+ public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged,
+ final boolean needsToDisplayLanguage, final boolean hasMultipleEnabledIMEsOrSubtypes) {
mNeedsToDisplayLanguage = needsToDisplayLanguage;
mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes;
final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator;
@@ -861,7 +894,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
mNeedsToDisplayLanguage = false;
} else {
if (subtypeChanged && needsToDisplayLanguage) {
- setLanguageOnSpacebarAnimAlpha(ALPHA_OPAQUE);
+ setLanguageOnSpacebarAnimAlpha(Constants.Color.ALPHA_OPAQUE);
if (animator.isStarted()) {
animator.cancel();
}
@@ -875,14 +908,15 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
invalidateKey(mSpaceKey);
}
- public void updateAutoCorrectionState(boolean isAutoCorrection) {
+ public void updateAutoCorrectionState(final boolean isAutoCorrection) {
if (!mAutoCorrectionSpacebarLedEnabled) return;
mAutoCorrectionSpacebarLedOn = isAutoCorrection;
invalidateKey(mSpaceKey);
}
@Override
- protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
+ protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
+ final KeyDrawParams params) {
if (key.altCodeWhileTyping() && key.isEnabled()) {
params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
}
@@ -900,7 +934,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
}
- private boolean fitsTextIntoWidth(final int width, String text, Paint paint) {
+ private boolean fitsTextIntoWidth(final int width, final String text, final Paint paint) {
paint.setTextScaleX(1.0f);
final float textWidth = getLabelWidth(text, paint);
if (textWidth < width) return true;
@@ -913,7 +947,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
// Layout language name on spacebar.
- private String layoutLanguageOnSpacebar(Paint paint, InputMethodSubtype subtype,
+ private String layoutLanguageOnSpacebar(final Paint paint, final InputMethodSubtype subtype,
final int width) {
// Choose appropriate language name to fit into the width.
String text = getFullDisplayName(subtype, getResources());
@@ -934,7 +968,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
return "";
}
- private void drawSpacebar(Key key, Canvas canvas, Paint paint) {
+ private void drawSpacebar(final Key key, final Canvas canvas, final Paint paint) {
final int width = key.mWidth;
final int height = key.mHeight;
@@ -989,7 +1023,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
// zz azerty T AZERTY AZERTY
// Get InputMethodSubtype's full display name in its locale.
- static String getFullDisplayName(InputMethodSubtype subtype, Resources res) {
+ static String getFullDisplayName(final InputMethodSubtype subtype, final Resources res) {
if (SubtypeLocale.isNoLanguage(subtype)) {
return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
}
@@ -998,7 +1032,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
// Get InputMethodSubtype's short display name in its locale.
- static String getShortDisplayName(InputMethodSubtype subtype) {
+ static String getShortDisplayName(final InputMethodSubtype subtype) {
if (SubtypeLocale.isNoLanguage(subtype)) {
return "";
}
@@ -1007,7 +1041,7 @@ public class LatinKeyboardView extends KeyboardView implements PointerTracker.Ke
}
// Get InputMethodSubtype's middle display name in its locale.
- static String getMiddleDisplayName(InputMethodSubtype subtype) {
+ static String getMiddleDisplayName(final InputMethodSubtype subtype) {
if (SubtypeLocale.isNoLanguage(subtype)) {
return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
}
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
index cd4e3001e..a2001cb8f 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysDetector.java
@@ -16,7 +16,7 @@
package com.android.inputmethod.keyboard;
-public class MoreKeysDetector extends KeyDetector {
+public final class MoreKeysDetector extends KeyDetector {
private final int mSlideAllowanceSquare;
private final int mSlideAllowanceSquareTop;
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
index a3741a2d8..d7d4be40b 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
@@ -20,15 +20,17 @@ import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.view.View;
-import com.android.inputmethod.keyboard.internal.KeySpecParser.MoreKeySpec;
+import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
+import com.android.inputmethod.keyboard.internal.KeyboardParams;
+import com.android.inputmethod.keyboard.internal.MoreKeySpec;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.StringUtils;
-public class MoreKeysKeyboard extends Keyboard {
+public final class MoreKeysKeyboard extends Keyboard {
private final int mDefaultKeyCoordX;
- MoreKeysKeyboard(Builder.MoreKeysKeyboardParams params) {
+ MoreKeysKeyboard(final MoreKeysKeyboardParams params) {
super(params);
mDefaultKeyCoordX = params.getDefaultKeyCoordX() + params.mDefaultKeyWidth / 2;
}
@@ -37,228 +39,231 @@ public class MoreKeysKeyboard extends Keyboard {
return mDefaultKeyCoordX;
}
- public static class Builder extends Keyboard.Builder<Builder.MoreKeysKeyboardParams> {
- private final Key mParentKey;
- private final Drawable mDivider;
-
- private static final float LABEL_PADDING_RATIO = 0.2f;
- private static final float DIVIDER_RATIO = 0.2f;
+ /* package for test */
+ static class MoreKeysKeyboardParams extends KeyboardParams {
+ public boolean mIsFixedOrder;
+ /* package */int mTopRowAdjustment;
+ public int mNumRows;
+ public int mNumColumns;
+ public int mTopKeys;
+ public int mLeftKeys;
+ public int mRightKeys; // includes default key.
+ public int mDividerWidth;
+ public int mColumnWidth;
+
+ public MoreKeysKeyboardParams() {
+ super();
+ }
- public static class MoreKeysKeyboardParams extends Keyboard.Params {
- public boolean mIsFixedOrder;
- /* package */int mTopRowAdjustment;
- public int mNumRows;
- public int mNumColumns;
- public int mTopKeys;
- public int mLeftKeys;
- public int mRightKeys; // includes default key.
- public int mDividerWidth;
- public int mColumnWidth;
-
- public MoreKeysKeyboardParams() {
- super();
+ /**
+ * Set keyboard parameters of more keys keyboard.
+ *
+ * @param numKeys number of keys in this more keys keyboard.
+ * @param maxColumns number of maximum columns of this more keys keyboard.
+ * @param keyWidth more keys keyboard key width in pixel, including horizontal gap.
+ * @param rowHeight more keys keyboard row height in pixel, including vertical gap.
+ * @param coordXInParent coordinate x of the key preview in parent keyboard.
+ * @param parentKeyboardWidth parent keyboard width in pixel.
+ * @param isFixedColumnOrder if true, more keys should be laid out in fixed order.
+ * @param dividerWidth width of divider, zero for no dividers.
+ */
+ public void setParameters(final int numKeys, final int maxColumns, final int keyWidth,
+ final int rowHeight, final int coordXInParent, final int parentKeyboardWidth,
+ final boolean isFixedColumnOrder, final int dividerWidth) {
+ mIsFixedOrder = isFixedColumnOrder;
+ if (parentKeyboardWidth / keyWidth < maxColumns) {
+ throw new IllegalArgumentException(
+ "Keyboard is too small to hold more keys keyboard: "
+ + parentKeyboardWidth + " " + keyWidth + " " + maxColumns);
}
-
- /**
- * Set keyboard parameters of more keys keyboard.
- *
- * @param numKeys number of keys in this more keys keyboard.
- * @param maxColumns number of maximum columns of this more keys keyboard.
- * @param keyWidth more keys keyboard key width in pixel, including horizontal gap.
- * @param rowHeight more keys keyboard row height in pixel, including vertical gap.
- * @param coordXInParent coordinate x of the key preview in parent keyboard.
- * @param parentKeyboardWidth parent keyboard width in pixel.
- * @param isFixedColumnOrder if true, more keys should be laid out in fixed order.
- * @param dividerWidth width of divider, zero for no dividers.
- */
- public void setParameters(int numKeys, int maxColumns, int keyWidth, int rowHeight,
- int coordXInParent, int parentKeyboardWidth, boolean isFixedColumnOrder,
- int dividerWidth) {
- mIsFixedOrder = isFixedColumnOrder;
- if (parentKeyboardWidth / keyWidth < maxColumns) {
- throw new IllegalArgumentException(
- "Keyboard is too small to hold more keys keyboard: "
- + parentKeyboardWidth + " " + keyWidth + " " + maxColumns);
- }
- mDefaultKeyWidth = keyWidth;
- mDefaultRowHeight = rowHeight;
-
- final int numRows = (numKeys + maxColumns - 1) / maxColumns;
- mNumRows = numRows;
- final int numColumns = mIsFixedOrder ? Math.min(numKeys, maxColumns)
- : getOptimizedColumns(numKeys, maxColumns);
- mNumColumns = numColumns;
- final int topKeys = numKeys % numColumns;
- mTopKeys = topKeys == 0 ? numColumns : topKeys;
-
- final int numLeftKeys = (numColumns - 1) / 2;
- final int numRightKeys = numColumns - numLeftKeys; // including default key.
- // Maximum number of keys we can layout both side of the parent key
- final int maxLeftKeys = coordXInParent / keyWidth;
- final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth;
- int leftKeys, rightKeys;
- if (numLeftKeys > maxLeftKeys) {
- leftKeys = maxLeftKeys;
- rightKeys = numColumns - leftKeys;
- } else if (numRightKeys > maxRightKeys + 1) {
- rightKeys = maxRightKeys + 1; // include default key
- leftKeys = numColumns - rightKeys;
- } else {
- leftKeys = numLeftKeys;
- rightKeys = numRightKeys;
- }
- // If the left keys fill the left side of the parent key, entire more keys keyboard
- // should be shifted to the right unless the parent key is on the left edge.
- if (maxLeftKeys == leftKeys && leftKeys > 0) {
- leftKeys--;
- rightKeys++;
- }
- // If the right keys fill the right side of the parent key, entire more keys
- // should be shifted to the left unless the parent key is on the right edge.
- if (maxRightKeys == rightKeys - 1 && rightKeys > 1) {
- leftKeys++;
- rightKeys--;
- }
- mLeftKeys = leftKeys;
- mRightKeys = rightKeys;
-
- // Adjustment of the top row.
- mTopRowAdjustment = mIsFixedOrder ? getFixedOrderTopRowAdjustment()
- : getAutoOrderTopRowAdjustment();
- mDividerWidth = dividerWidth;
- mColumnWidth = mDefaultKeyWidth + mDividerWidth;
- mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth;
- // Need to subtract the bottom row's gutter only.
- mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap
- + mTopPadding + mBottomPadding;
+ mDefaultKeyWidth = keyWidth;
+ mDefaultRowHeight = rowHeight;
+
+ final int numRows = (numKeys + maxColumns - 1) / maxColumns;
+ mNumRows = numRows;
+ final int numColumns = mIsFixedOrder ? Math.min(numKeys, maxColumns)
+ : getOptimizedColumns(numKeys, maxColumns);
+ mNumColumns = numColumns;
+ final int topKeys = numKeys % numColumns;
+ mTopKeys = topKeys == 0 ? numColumns : topKeys;
+
+ final int numLeftKeys = (numColumns - 1) / 2;
+ final int numRightKeys = numColumns - numLeftKeys; // including default key.
+ // Maximum number of keys we can layout both side of the parent key
+ final int maxLeftKeys = coordXInParent / keyWidth;
+ final int maxRightKeys = (parentKeyboardWidth - coordXInParent) / keyWidth;
+ int leftKeys, rightKeys;
+ if (numLeftKeys > maxLeftKeys) {
+ leftKeys = maxLeftKeys;
+ rightKeys = numColumns - leftKeys;
+ } else if (numRightKeys > maxRightKeys + 1) {
+ rightKeys = maxRightKeys + 1; // include default key
+ leftKeys = numColumns - rightKeys;
+ } else {
+ leftKeys = numLeftKeys;
+ rightKeys = numRightKeys;
}
-
- private int getFixedOrderTopRowAdjustment() {
- if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns
- || mLeftKeys == 0 || mRightKeys == 1) {
- return 0;
- }
- return -1;
+ // If the left keys fill the left side of the parent key, entire more keys keyboard
+ // should be shifted to the right unless the parent key is on the left edge.
+ if (maxLeftKeys == leftKeys && leftKeys > 0) {
+ leftKeys--;
+ rightKeys++;
}
-
- private int getAutoOrderTopRowAdjustment() {
- if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2
- || mLeftKeys == 0 || mRightKeys == 1) {
- return 0;
- }
- return -1;
+ // If the right keys fill the right side of the parent key, entire more keys
+ // should be shifted to the left unless the parent key is on the right edge.
+ if (maxRightKeys == rightKeys - 1 && rightKeys > 1) {
+ leftKeys++;
+ rightKeys--;
}
+ mLeftKeys = leftKeys;
+ mRightKeys = rightKeys;
+
+ // Adjustment of the top row.
+ mTopRowAdjustment = mIsFixedOrder ? getFixedOrderTopRowAdjustment()
+ : getAutoOrderTopRowAdjustment();
+ mDividerWidth = dividerWidth;
+ mColumnWidth = mDefaultKeyWidth + mDividerWidth;
+ mBaseWidth = mOccupiedWidth = mNumColumns * mColumnWidth - mDividerWidth;
+ // Need to subtract the bottom row's gutter only.
+ mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight - mVerticalGap
+ + mTopPadding + mBottomPadding;
+ }
- // Return key position according to column count (0 is default).
- /* package */int getColumnPos(int n) {
- return mIsFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
+ private int getFixedOrderTopRowAdjustment() {
+ if (mNumRows == 1 || mTopKeys % 2 == 1 || mTopKeys == mNumColumns
+ || mLeftKeys == 0 || mRightKeys == 1) {
+ return 0;
}
+ return -1;
+ }
- private int getFixedOrderColumnPos(int n) {
- final int col = n % mNumColumns;
- final int row = n / mNumColumns;
- if (!isTopRow(row)) {
- return col - mLeftKeys;
- }
- final int rightSideKeys = mTopKeys / 2;
- final int leftSideKeys = mTopKeys - (rightSideKeys + 1);
- final int pos = col - leftSideKeys;
- final int numLeftKeys = mLeftKeys + mTopRowAdjustment;
- final int numRightKeys = mRightKeys - 1;
- if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) {
- return pos;
- } else if (numRightKeys < rightSideKeys) {
- return pos - (rightSideKeys - numRightKeys);
- } else { // numLeftKeys < leftSideKeys
- return pos + (leftSideKeys - numLeftKeys);
- }
+ private int getAutoOrderTopRowAdjustment() {
+ if (mNumRows == 1 || mTopKeys == 1 || mNumColumns % 2 == mTopKeys % 2
+ || mLeftKeys == 0 || mRightKeys == 1) {
+ return 0;
}
+ return -1;
+ }
- private int getAutomaticColumnPos(int n) {
- final int col = n % mNumColumns;
- final int row = n / mNumColumns;
- int leftKeys = mLeftKeys;
- if (isTopRow(row)) {
- leftKeys += mTopRowAdjustment;
- }
- if (col == 0) {
- // default position.
- return 0;
- }
+ // Return key position according to column count (0 is default).
+ /* package */int getColumnPos(final int n) {
+ return mIsFixedOrder ? getFixedOrderColumnPos(n) : getAutomaticColumnPos(n);
+ }
- int pos = 0;
- int right = 1; // include default position key.
- int left = 0;
- int i = 0;
- while (true) {
- // Assign right key if available.
- if (right < mRightKeys) {
- pos = right;
- right++;
- i++;
- }
- if (i >= col)
- break;
- // Assign left key if available.
- if (left < leftKeys) {
- left++;
- pos = -left;
- i++;
- }
- if (i >= col)
- break;
- }
+ private int getFixedOrderColumnPos(final int n) {
+ final int col = n % mNumColumns;
+ final int row = n / mNumColumns;
+ if (!isTopRow(row)) {
+ return col - mLeftKeys;
+ }
+ final int rightSideKeys = mTopKeys / 2;
+ final int leftSideKeys = mTopKeys - (rightSideKeys + 1);
+ final int pos = col - leftSideKeys;
+ final int numLeftKeys = mLeftKeys + mTopRowAdjustment;
+ final int numRightKeys = mRightKeys - 1;
+ if (numRightKeys >= rightSideKeys && numLeftKeys >= leftSideKeys) {
return pos;
+ } else if (numRightKeys < rightSideKeys) {
+ return pos - (rightSideKeys - numRightKeys);
+ } else { // numLeftKeys < leftSideKeys
+ return pos + (leftSideKeys - numLeftKeys);
}
+ }
- private static int getTopRowEmptySlots(int numKeys, int numColumns) {
- final int remainings = numKeys % numColumns;
- return remainings == 0 ? 0 : numColumns - remainings;
+ private int getAutomaticColumnPos(final int n) {
+ final int col = n % mNumColumns;
+ final int row = n / mNumColumns;
+ int leftKeys = mLeftKeys;
+ if (isTopRow(row)) {
+ leftKeys += mTopRowAdjustment;
+ }
+ if (col == 0) {
+ // default position.
+ return 0;
}
- private int getOptimizedColumns(int numKeys, int maxColumns) {
- int numColumns = Math.min(numKeys, maxColumns);
- while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) {
- numColumns--;
+ int pos = 0;
+ int right = 1; // include default position key.
+ int left = 0;
+ int i = 0;
+ while (true) {
+ // Assign right key if available.
+ if (right < mRightKeys) {
+ pos = right;
+ right++;
+ i++;
}
- return numColumns;
+ if (i >= col)
+ break;
+ // Assign left key if available.
+ if (left < leftKeys) {
+ left++;
+ pos = -left;
+ i++;
+ }
+ if (i >= col)
+ break;
}
+ return pos;
+ }
- public int getDefaultKeyCoordX() {
- return mLeftKeys * mColumnWidth;
- }
+ private static int getTopRowEmptySlots(final int numKeys, final int numColumns) {
+ final int remainings = numKeys % numColumns;
+ return remainings == 0 ? 0 : numColumns - remainings;
+ }
- public int getX(int n, int row) {
- final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX();
- if (isTopRow(row)) {
- return x + mTopRowAdjustment * (mColumnWidth / 2);
- }
- return x;
+ private int getOptimizedColumns(final int numKeys, final int maxColumns) {
+ int numColumns = Math.min(numKeys, maxColumns);
+ while (getTopRowEmptySlots(numKeys, numColumns) >= mNumRows) {
+ numColumns--;
}
+ return numColumns;
+ }
- public int getY(int row) {
- return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
- }
+ public int getDefaultKeyCoordX() {
+ return mLeftKeys * mColumnWidth;
+ }
- public void markAsEdgeKey(Key key, int row) {
- if (row == 0)
- key.markAsTopEdge(this);
- if (isTopRow(row))
- key.markAsBottomEdge(this);
+ public int getX(final int n, final int row) {
+ final int x = getColumnPos(n) * mColumnWidth + getDefaultKeyCoordX();
+ if (isTopRow(row)) {
+ return x + mTopRowAdjustment * (mColumnWidth / 2);
}
+ return x;
+ }
- private boolean isTopRow(int rowCount) {
- return mNumRows > 1 && rowCount == mNumRows - 1;
- }
+ public int getY(final int row) {
+ return (mNumRows - 1 - row) * mDefaultRowHeight + mTopPadding;
}
+ public void markAsEdgeKey(final Key key, final int row) {
+ if (row == 0)
+ key.markAsTopEdge(this);
+ if (isTopRow(row))
+ key.markAsBottomEdge(this);
+ }
+
+ private boolean isTopRow(final int rowCount) {
+ return mNumRows > 1 && rowCount == mNumRows - 1;
+ }
+ }
+
+ public static class Builder extends KeyboardBuilder<MoreKeysKeyboardParams> {
+ private final Key mParentKey;
+ private final Drawable mDivider;
+
+ private static final float LABEL_PADDING_RATIO = 0.2f;
+ private static final float DIVIDER_RATIO = 0.2f;
+
+
/**
* The builder of MoreKeysKeyboard.
* @param containerView the container of {@link MoreKeysKeyboardView}.
* @param parentKey the {@link Key} that invokes more keys keyboard.
* @param parentKeyboardView the {@link KeyboardView} that contains the parentKey.
*/
- public Builder(View containerView, Key parentKey, KeyboardView parentKeyboardView) {
+ public Builder(final View containerView, final Key parentKey,
+ final KeyboardView parentKeyboardView) {
super(containerView.getContext(), new MoreKeysKeyboardParams());
final Keyboard parentKeyboard = parentKeyboardView.getKeyboard();
load(parentKeyboard.mMoreKeysTemplate, parentKeyboard.mId);
@@ -300,14 +305,14 @@ public class MoreKeysKeyboard extends Keyboard {
dividerWidth);
}
- private static int getMaxKeyWidth(KeyboardView view, Key parentKey, int minKeyWidth) {
+ private static int getMaxKeyWidth(final KeyboardView view, final Key parentKey,
+ final int minKeyWidth) {
final int padding = (int)(view.getResources()
.getDimension(R.dimen.more_keys_keyboard_key_horizontal_padding)
+ (parentKey.hasLabelsInMoreKeys() ? minKeyWidth * LABEL_PADDING_RATIO : 0));
final Paint paint = view.newDefaultLabelPaint();
- paint.setTextSize(parentKey.hasLabelsInMoreKeys()
- ? view.mKeyDrawParams.mKeyLabelSize
- : view.mKeyDrawParams.mKeyLetterSize);
+ paint.setTypeface(parentKey.selectTypeface(view.mKeyDrawParams));
+ paint.setTextSize(parentKey.selectMoreKeyTextSize(view.mKeyDrawParams));
int maxWidth = minKeyWidth;
for (final MoreKeySpec spec : parentKey.mMoreKeys) {
final String label = spec.mLabel;
@@ -322,24 +327,6 @@ public class MoreKeysKeyboard extends Keyboard {
return maxWidth;
}
- private static class MoreKeyDivider extends Key.Spacer {
- private final Drawable mIcon;
-
- public MoreKeyDivider(MoreKeysKeyboardParams params, Drawable icon, int x, int y) {
- super(params, x, y, params.mDividerWidth, params.mDefaultRowHeight);
- mIcon = icon;
- }
-
- @Override
- public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) {
- // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the
- // constructor.
- // TODO: Drawable itself should have an alpha value.
- mIcon.setAlpha(128);
- return mIcon;
- }
- }
-
@Override
public MoreKeysKeyboard build() {
final MoreKeysKeyboardParams params = mParams;
@@ -368,4 +355,23 @@ public class MoreKeysKeyboard extends Keyboard {
return new MoreKeysKeyboard(params);
}
}
+
+ private static class MoreKeyDivider extends Key.Spacer {
+ private final Drawable mIcon;
+
+ public MoreKeyDivider(final MoreKeysKeyboardParams params, final Drawable icon,
+ final int x, final int y) {
+ super(params, x, y, params.mDividerWidth, params.mDefaultRowHeight);
+ mIcon = icon;
+ }
+
+ @Override
+ public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
+ // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the
+ // constructor.
+ // TODO: Drawable itself should have an alpha value.
+ mIcon.setAlpha(128);
+ return mIcon;
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
index be7644fb5..a50617693 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
@@ -25,13 +25,15 @@ import android.widget.PopupWindow;
import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.InputPointers;
import com.android.inputmethod.latin.R;
/**
* A view that renders a virtual {@link MoreKeysKeyboard}. It handles rendering of keys and
* detecting key presses and touch movements.
*/
-public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel {
+public final class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel {
private final int[] mCoordinates = new int[2];
private final KeyDetector mKeyDetector;
@@ -49,7 +51,8 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
public void onCodeInput(int primaryCode, int x, int y) {
// Because a more keys keyboard doesn't need proximity characters correction, we don't
// send touch event coordinates.
- mListener.onCodeInput(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE);
+ mListener.onCodeInput(
+ primaryCode, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
}
@Override
@@ -58,6 +61,21 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
}
@Override
+ public void onStartBatchInput() {
+ mListener.onStartBatchInput();
+ }
+
+ @Override
+ public void onUpdateBatchInput(InputPointers batchPointers) {
+ mListener.onUpdateBatchInput(batchPointers);
+ }
+
+ @Override
+ public void onEndBatchInput(InputPointers batchPointers) {
+ mListener.onEndBatchInput(batchPointers);
+ }
+
+ @Override
public void onCancelInput() {
mListener.onCancelInput();
}
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index babf6ec99..d0f7cb276 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -16,25 +16,37 @@
package com.android.inputmethod.keyboard;
+import android.content.res.TypedArray;
import android.os.SystemClock;
import android.util.Log;
import android.view.MotionEvent;
-import android.view.View;
-import android.widget.TextView;
+import com.android.inputmethod.accessibility.AccessibilityUtils;
+import com.android.inputmethod.keyboard.internal.GestureStroke;
+import com.android.inputmethod.keyboard.internal.GestureStroke.GestureStrokeParams;
+import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints;
import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.InputPointers;
import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.ResearchLogger;
+import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
import java.util.ArrayList;
-public class PointerTracker {
+public final class PointerTracker implements PointerTrackerQueue.Element {
private static final String TAG = PointerTracker.class.getSimpleName();
private static final boolean DEBUG_EVENT = false;
private static final boolean DEBUG_MOVE_EVENT = false;
private static final boolean DEBUG_LISTENER = false;
- private static boolean DEBUG_MODE = LatinImeLogger.sDBG;
+ private static boolean DEBUG_MODE = LatinImeLogger.sDBG || DEBUG_EVENT;
+
+ /** True if {@link PointerTracker}s should handle gesture events. */
+ private static boolean sShouldHandleGesture = false;
+ private static boolean sMainDictionaryAvailable = false;
+ private static boolean sGestureHandlingEnabledByInputField = false;
+ private static boolean sGestureHandlingEnabledByUser = false;
public interface KeyEventHandler {
/**
@@ -65,13 +77,13 @@ public class PointerTracker {
public interface DrawingProxy extends MoreKeysPanel.Controller {
public void invalidateKey(Key key);
- public TextView inflateKeyPreviewText();
public void showKeyPreview(PointerTracker tracker);
public void dismissKeyPreview(PointerTracker tracker);
+ public void showGesturePreviewTrail(PointerTracker tracker, boolean isOldestTracker);
}
public interface TimerProxy {
- public void startTypingStateTimer();
+ public void startTypingStateTimer(Key typedKey);
public boolean isTypingState();
public void startKeyRepeatTimer(PointerTracker tracker);
public void startLongPressTimer(PointerTracker tracker);
@@ -84,7 +96,7 @@ public class PointerTracker {
public static class Adapter implements TimerProxy {
@Override
- public void startTypingStateTimer() {}
+ public void startTypingStateTimer(Key typedKey) {}
@Override
public boolean isTypingState() { return false; }
@Override
@@ -106,12 +118,44 @@ public class PointerTracker {
}
}
+ static final class PointerTrackerParams {
+ public final boolean mSlidingKeyInputEnabled;
+ public final int mTouchNoiseThresholdTime;
+ public final int mTouchNoiseThresholdDistance;
+ public final int mSuppressKeyPreviewAfterBatchInputDuration;
+
+ public static final PointerTrackerParams DEFAULT = new PointerTrackerParams();
+
+ private PointerTrackerParams() {
+ mSlidingKeyInputEnabled = false;
+ mTouchNoiseThresholdTime = 0;
+ mTouchNoiseThresholdDistance = 0;
+ mSuppressKeyPreviewAfterBatchInputDuration = 0;
+ }
+
+ public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) {
+ mSlidingKeyInputEnabled = mainKeyboardViewAttr.getBoolean(
+ R.styleable.MainKeyboardView_slidingKeyInputEnable, false);
+ mTouchNoiseThresholdTime = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_touchNoiseThresholdTime, 0);
+ mTouchNoiseThresholdDistance = mainKeyboardViewAttr.getDimensionPixelSize(
+ R.styleable.MainKeyboardView_touchNoiseThresholdDistance, 0);
+ mSuppressKeyPreviewAfterBatchInputDuration = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration, 0);
+ }
+ }
+
// Parameters for pointer handling.
- private static LatinKeyboardView.PointerTrackerParams sParams;
- private static int sTouchNoiseThresholdDistanceSquared;
+ private static PointerTrackerParams sParams;
+ private static GestureStrokeParams sGestureStrokeParams;
private static boolean sNeedsPhantomSuddenMoveEventHack;
+ // Move this threshold to resource.
+ // TODO: Device specific parameter would be better for device specific hack?
+ private static final float PHANTOM_SUDDEN_MOVE_THRESHOLD = 0.25f; // in keyWidth
+ // This hack might be device specific.
+ private static final boolean sNeedsProximateBogusDownMoveUpEventHack = true;
- private static final ArrayList<PointerTracker> sTrackers = new ArrayList<PointerTracker>();
+ private static final ArrayList<PointerTracker> sTrackers = CollectionUtils.newArrayList();
private static PointerTrackerQueue sPointerTrackerQueue;
public final int mPointerId;
@@ -122,8 +166,126 @@ public class PointerTracker {
private KeyboardActionListener mListener = EMPTY_LISTENER;
private Keyboard mKeyboard;
- private int mKeyQuarterWidthSquared;
- private final TextView mKeyPreviewText;
+ private int mPhantonSuddenMoveThreshold;
+ private final BogusMoveEventDetector mBogusMoveEventDetector = new BogusMoveEventDetector();
+
+ private boolean mIsDetectingGesture = false; // per PointerTracker.
+ private static boolean sInGesture = false;
+ private static long sGestureFirstDownTime;
+ private static TimeRecorder sTimeRecorder;
+ private static final InputPointers sAggregratedPointers = new InputPointers(
+ GestureStroke.DEFAULT_CAPACITY);
+ private static int sLastRecognitionPointSize = 0; // synchronized using sAggregratedPointers
+ private static long sLastRecognitionTime = 0; // synchronized using sAggregratedPointers
+
+ static final class BogusMoveEventDetector {
+ // Move these thresholds to resource.
+ // These thresholds' unit is a diagonal length of a key.
+ private static final float BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD = 0.53f;
+ private static final float BOGUS_MOVE_RADIUS_THRESHOLD = 1.14f;
+
+ private int mAccumulatedDistanceThreshold;
+ private int mRadiusThreshold;
+
+ // Accumulated distance from actual and artificial down keys.
+ /* package */ int mAccumulatedDistanceFromDownKey;
+ private int mActualDownX;
+ private int mActualDownY;
+
+ public void setKeyboardGeometry(final int keyWidth, final int keyHeight) {
+ final float keyDiagonal = (float)Math.hypot(keyWidth, keyHeight);
+ mAccumulatedDistanceThreshold = (int)(
+ keyDiagonal * BOGUS_MOVE_ACCUMULATED_DISTANCE_THRESHOLD);
+ mRadiusThreshold = (int)(keyDiagonal * BOGUS_MOVE_RADIUS_THRESHOLD);
+ }
+
+ public void onActualDownEvent(final int x, final int y) {
+ mActualDownX = x;
+ mActualDownY = y;
+ }
+
+ public void onDownKey() {
+ mAccumulatedDistanceFromDownKey = 0;
+ }
+
+ public void onMoveKey(final int distance) {
+ mAccumulatedDistanceFromDownKey += distance;
+ }
+
+ public boolean hasTraveledLongDistance(final int x, final int y) {
+ final int dx = Math.abs(x - mActualDownX);
+ final int dy = Math.abs(y - mActualDownY);
+ // A bogus move event should be a horizontal movement. A vertical movement might be
+ // a sloppy typing and should be ignored.
+ return dx >= dy && mAccumulatedDistanceFromDownKey >= mAccumulatedDistanceThreshold;
+ }
+
+ /* package */ int getDistanceFromDownEvent(final int x, final int y) {
+ return getDistance(x, y, mActualDownX, mActualDownY);
+ }
+
+ public boolean isCloseToActualDownEvent(final int x, final int y) {
+ return getDistanceFromDownEvent(x, y) < mRadiusThreshold;
+ }
+ }
+
+ static final class TimeRecorder {
+ private final int mSuppressKeyPreviewAfterBatchInputDuration;
+ private final int mStaticTimeThresholdAfterFastTyping; // msec
+ private long mLastTypingTime;
+ private long mLastLetterTypingTime;
+ private long mLastBatchInputTime;
+
+ public TimeRecorder(final PointerTrackerParams pointerTrackerParams,
+ final GestureStrokeParams gestureStrokeParams) {
+ mSuppressKeyPreviewAfterBatchInputDuration =
+ pointerTrackerParams.mSuppressKeyPreviewAfterBatchInputDuration;
+ mStaticTimeThresholdAfterFastTyping =
+ gestureStrokeParams.mStaticTimeThresholdAfterFastTyping;
+ }
+
+ public boolean isInFastTyping(final long eventTime) {
+ final long elapsedTimeSinceLastLetterTyping = eventTime - mLastLetterTypingTime;
+ return elapsedTimeSinceLastLetterTyping < mStaticTimeThresholdAfterFastTyping;
+ }
+
+ private boolean wasLastInputTyping() {
+ return mLastTypingTime >= mLastBatchInputTime;
+ }
+
+ public void onCodeInput(final int code, final long eventTime) {
+ // Record the letter typing time when
+ // 1. Letter keys are typed successively without any batch input in between.
+ // 2. A letter key is typed within the threshold time since the last any key typing.
+ // 3. A non-letter key is typed within the threshold time since the last letter key
+ // typing.
+ if (Character.isLetter(code)) {
+ if (wasLastInputTyping()
+ || eventTime - mLastTypingTime < mStaticTimeThresholdAfterFastTyping) {
+ mLastLetterTypingTime = eventTime;
+ }
+ } else {
+ if (eventTime - mLastLetterTypingTime < mStaticTimeThresholdAfterFastTyping) {
+ // This non-letter typing should be treated as a part of fast typing.
+ mLastLetterTypingTime = eventTime;
+ }
+ }
+ mLastTypingTime = eventTime;
+ }
+
+ public void onEndBatchInput(final long eventTime) {
+ mLastBatchInputTime = eventTime;
+ }
+
+ public long getLastLetterTypingTime() {
+ return mLastLetterTypingTime;
+ }
+
+ public boolean needsToSuppressKeyPreviewPopup(final long eventTime) {
+ return !wasLastInputTyping()
+ && eventTime - mLastBatchInputTime < mSuppressKeyPreviewAfterBatchInputDuration;
+ }
+ }
// The position and time at which first down event occurred.
private long mDownTime;
@@ -148,22 +310,21 @@ public class PointerTracker {
// true if this pointer has been long-pressed and is showing a more keys panel.
private boolean mIsShowingMoreKeysPanel;
- // true if this pointer is repeatable key
- private boolean mIsRepeatableKey;
-
- // true if this pointer is in sliding key input
+ // 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;
- // true if sliding key is allowed.
+ // true if a sliding key input is allowed.
private boolean mIsAllowedSlidingKeyInput;
- // ignore modifier key if true
- private boolean mIgnoreModifierKey;
-
// Empty {@link KeyboardActionListener}
private static final KeyboardActionListener EMPTY_LISTENER =
new KeyboardActionListener.Adapter();
+ private final GestureStrokeWithPreviewPoints mGestureStrokeWithPreviewPoints;
+
public static void init(boolean hasDistinctMultitouch,
boolean needsPhantomSuddenMoveEventHack) {
if (hasDistinctMultitouch) {
@@ -172,17 +333,36 @@ public class PointerTracker {
sPointerTrackerQueue = null;
}
sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack;
+ sParams = PointerTrackerParams.DEFAULT;
+ sGestureStrokeParams = GestureStrokeParams.DEFAULT;
+ sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
+ }
+
+ public static void setParameters(final TypedArray mainKeyboardViewAttr) {
+ sParams = new PointerTrackerParams(mainKeyboardViewAttr);
+ sGestureStrokeParams = new GestureStrokeParams(mainKeyboardViewAttr);
+ sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
+ }
+
+ private static void updateGestureHandlingMode() {
+ sShouldHandleGesture = sMainDictionaryAvailable
+ && sGestureHandlingEnabledByInputField
+ && sGestureHandlingEnabledByUser
+ && !AccessibilityUtils.getInstance().isTouchExplorationEnabled();
+ }
- setParameters(LatinKeyboardView.PointerTrackerParams.DEFAULT);
+ // Note that this method is called from a non-UI thread.
+ public static void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) {
+ sMainDictionaryAvailable = mainDictionaryAvailable;
+ updateGestureHandlingMode();
}
- public static void setParameters(LatinKeyboardView.PointerTrackerParams params) {
- sParams = params;
- sTouchNoiseThresholdDistanceSquared = (int)(
- params.mTouchNoiseThresholdDistance * params.mTouchNoiseThresholdDistance);
+ public static void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) {
+ sGestureHandlingEnabledByUser = gestureHandlingEnabledByUser;
+ updateGestureHandlingMode();
}
- public static PointerTracker getPointerTracker(final int id, KeyEventHandler handler) {
+ public static PointerTracker getPointerTracker(final int id, final KeyEventHandler handler) {
final ArrayList<PointerTracker> trackers = sTrackers;
// Create pointer trackers until we can get 'id+1'-th tracker, if needed.
@@ -198,53 +378,59 @@ public class PointerTracker {
return sPointerTrackerQueue != null ? sPointerTrackerQueue.isAnyInSlidingKeyInput() : false;
}
- public static void setKeyboardActionListener(KeyboardActionListener listener) {
- for (final PointerTracker tracker : sTrackers) {
+ public static void setKeyboardActionListener(final KeyboardActionListener listener) {
+ final int trackersSize = sTrackers.size();
+ for (int i = 0; i < trackersSize; ++i) {
+ final PointerTracker tracker = sTrackers.get(i);
tracker.mListener = listener;
}
}
- public static void setKeyDetector(KeyDetector keyDetector) {
- for (final PointerTracker tracker : sTrackers) {
+ public static void setKeyDetector(final KeyDetector keyDetector) {
+ final int trackersSize = sTrackers.size();
+ for (int i = 0; i < trackersSize; ++i) {
+ final PointerTracker tracker = sTrackers.get(i);
tracker.setKeyDetectorInner(keyDetector);
// Mark that keyboard layout has been changed.
tracker.mKeyboardLayoutHasBeenChanged = true;
}
+ final Keyboard keyboard = keyDetector.getKeyboard();
+ sGestureHandlingEnabledByInputField = !keyboard.mId.passwordInput();
+ updateGestureHandlingMode();
}
- public static void dismissAllKeyPreviews() {
- for (final PointerTracker tracker : sTrackers) {
- tracker.getKeyPreviewText().setVisibility(View.INVISIBLE);
+ public static void setReleasedKeyGraphicsToAllKeys() {
+ final int trackersSize = sTrackers.size();
+ for (int i = 0; i < trackersSize; ++i) {
+ final PointerTracker tracker = sTrackers.get(i);
tracker.setReleasedKeyGraphics(tracker.mCurrentKey);
}
}
- public PointerTracker(int id, KeyEventHandler handler) {
- if (handler == null)
+ private PointerTracker(final int id, final KeyEventHandler handler) {
+ if (handler == null) {
throw new NullPointerException();
+ }
mPointerId = id;
+ mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints(
+ id, sGestureStrokeParams);
setKeyDetectorInner(handler.getKeyDetector());
mListener = handler.getKeyboardActionListener();
mDrawingProxy = handler.getDrawingProxy();
mTimerProxy = handler.getTimerProxy();
- mKeyPreviewText = mDrawingProxy.inflateKeyPreviewText();
- }
-
- public TextView getKeyPreviewText() {
- return mKeyPreviewText;
}
// Returns true if keyboard has been changed by this callback.
- private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) {
- final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
- if (DEBUG_LISTENER) {
- Log.d(TAG, "onPress : " + KeyDetector.printableCode(key)
- + " ignoreModifier=" + ignoreModifierKey
- + " enabled=" + key.isEnabled());
+ private boolean callListenerOnPressAndCheckKeyboardLayoutChange(final Key key) {
+ if (sInGesture) {
+ return false;
}
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.pointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange(key,
- ignoreModifierKey);
+ final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
+ if (DEBUG_LISTENER) {
+ Log.d(TAG, String.format("[%d] onPress : %s%s%s", mPointerId,
+ KeyDetector.printableCode(key),
+ ignoreModifierKey ? " ignoreModifier" : "",
+ key.isEnabled() ? "" : " disabled"));
}
if (ignoreModifierKey) {
return false;
@@ -253,9 +439,7 @@ public class PointerTracker {
mListener.onPressKey(key.mCode);
final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
mKeyboardLayoutHasBeenChanged = false;
- if (!key.altCodeWhileTyping() && !key.isModifier()) {
- mTimerProxy.startTypingStateTimer();
- }
+ mTimerProxy.startTypingStateTimer(key);
return keyboardLayoutHasBeenChanged;
}
return false;
@@ -263,15 +447,17 @@ public class PointerTracker {
// Note that we need primaryCode argument because the keyboard may in shifted state and the
// primaryCode is different from {@link Key#mCode}.
- private void callListenerOnCodeInput(Key key, int primaryCode, int x, int y) {
- final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
+ private void callListenerOnCodeInput(final Key key, final int primaryCode, final int x,
+ final int y, final long eventTime) {
+ final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
final boolean altersCode = key.altCodeWhileTyping() && mTimerProxy.isTypingState();
- final int code = altersCode ? key.mAltCode : primaryCode;
+ final int code = altersCode ? key.getAltCode() : primaryCode;
if (DEBUG_LISTENER) {
- Log.d(TAG, "onCodeInput: " + Keyboard.printableCode(code) + " text=" + key.mOutputText
- + " x=" + x + " y=" + y
- + " ignoreModifier=" + ignoreModifierKey + " altersCode=" + altersCode
- + " enabled=" + key.isEnabled());
+ final String output = code == Keyboard.CODE_OUTPUT_TEXT
+ ? key.getOutputText() : Keyboard.printableCode(code);
+ Log.d(TAG, String.format("[%d] onCodeInput: %4d %4d %s%s%s", mPointerId, x, y,
+ output, ignoreModifierKey ? " ignoreModifier" : "",
+ altersCode ? " altersCode" : "", key.isEnabled() ? "" : " disabled"));
}
if (ProductionFlag.IS_EXPERIMENTAL) {
ResearchLogger.pointerTracker_callListenerOnCodeInput(key, x, y, ignoreModifierKey,
@@ -282,22 +468,28 @@ public class PointerTracker {
}
// Even if the key is disabled, it should respond if it is in the altCodeWhileTyping state.
if (key.isEnabled() || altersCode) {
+ sTimeRecorder.onCodeInput(code, eventTime);
if (code == Keyboard.CODE_OUTPUT_TEXT) {
- mListener.onTextInput(key.mOutputText);
+ mListener.onTextInput(key.getOutputText());
} else if (code != Keyboard.CODE_UNSPECIFIED) {
mListener.onCodeInput(code, x, y);
}
}
}
- // Note that we need primaryCode argument because the keyboard may in shifted state and the
+ // Note that we need primaryCode argument because the keyboard may be in shifted state and the
// primaryCode is different from {@link Key#mCode}.
- private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) {
- final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
+ private void callListenerOnRelease(final Key key, final int primaryCode,
+ final boolean withSliding) {
+ if (sInGesture) {
+ return;
+ }
+ final boolean ignoreModifierKey = mIsInSlidingKeyInputFromModifier && key.isModifier();
if (DEBUG_LISTENER) {
- Log.d(TAG, "onRelease : " + Keyboard.printableCode(primaryCode)
- + " sliding=" + withSliding + " ignoreModifier=" + ignoreModifierKey
- + " enabled="+ key.isEnabled());
+ Log.d(TAG, String.format("[%d] onRelease : %s%s%s%s", mPointerId,
+ Keyboard.printableCode(primaryCode),
+ withSliding ? " sliding" : "", ignoreModifierKey ? " ignoreModifier" : "",
+ key.isEnabled() ? "": " disabled"));
}
if (ProductionFlag.IS_EXPERIMENTAL) {
ResearchLogger.pointerTracker_callListenerOnRelease(key, primaryCode, withSliding,
@@ -312,21 +504,37 @@ public class PointerTracker {
}
private void callListenerOnCancelInput() {
- if (DEBUG_LISTENER)
- Log.d(TAG, "onCancelInput");
+ if (DEBUG_LISTENER) {
+ Log.d(TAG, String.format("[%d] onCancelInput", mPointerId));
+ }
if (ProductionFlag.IS_EXPERIMENTAL) {
ResearchLogger.pointerTracker_callListenerOnCancelInput();
}
mListener.onCancelInput();
}
- private void setKeyDetectorInner(KeyDetector keyDetector) {
+ private void setKeyDetectorInner(final KeyDetector keyDetector) {
+ final Keyboard keyboard = keyDetector.getKeyboard();
+ if (keyDetector == mKeyDetector && keyboard == mKeyboard) {
+ return;
+ }
mKeyDetector = keyDetector;
mKeyboard = keyDetector.getKeyboard();
- final int keyQuarterWidth = mKeyboard.mMostCommonKeyWidth / 4;
- mKeyQuarterWidthSquared = keyQuarterWidth * keyQuarterWidth;
+ final int keyWidth = mKeyboard.mMostCommonKeyWidth;
+ final int keyHeight = mKeyboard.mMostCommonKeyHeight;
+ mGestureStrokeWithPreviewPoints.setKeyboardGeometry(keyWidth);
+ final Key newKey = mKeyDetector.detectHitKey(mKeyX, mKeyY);
+ if (newKey != mCurrentKey) {
+ if (mDrawingProxy != null) {
+ setReleasedKeyGraphics(mCurrentKey);
+ }
+ // Keep {@link #mCurrentKey} that comes from previous keyboard.
+ }
+ mPhantonSuddenMoveThreshold = (int)(keyWidth * PHANTOM_SUDDEN_MOVE_THRESHOLD);
+ mBogusMoveEventDetector.setKeyboardGeometry(keyWidth, keyHeight);
}
+ @Override
public boolean isInSlidingKeyInput() {
return mIsInSlidingKeyInput;
}
@@ -335,15 +543,16 @@ public class PointerTracker {
return mCurrentKey;
}
+ @Override
public boolean isModifier() {
return mCurrentKey != null && mCurrentKey.isModifier();
}
- public Key getKeyOn(int x, int y) {
+ public Key getKeyOn(final int x, final int y) {
return mKeyDetector.detectHitKey(x, y);
}
- private void setReleasedKeyGraphics(Key key) {
+ private void setReleasedKeyGraphics(final Key key) {
mDrawingProxy.dismissKeyPreview(this);
if (key == null) {
return;
@@ -361,20 +570,25 @@ public class PointerTracker {
}
if (key.altCodeWhileTyping()) {
- final int altCode = key.mAltCode;
+ final int altCode = key.getAltCode();
final Key altKey = mKeyboard.getKey(altCode);
if (altKey != null) {
updateReleaseKeyGraphics(altKey);
}
for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
- if (k != key && k.mAltCode == altCode) {
+ if (k != key && k.getAltCode() == altCode) {
updateReleaseKeyGraphics(k);
}
}
}
}
- private void setPressedKeyGraphics(Key key) {
+ private static boolean needsToSuppressKeyPreviewPopup(final long eventTime) {
+ if (!sShouldHandleGesture) return false;
+ return sTimeRecorder.needsToSuppressKeyPreviewPopup(eventTime);
+ }
+
+ private void setPressedKeyGraphics(final Key key, final long eventTime) {
if (key == null) {
return;
}
@@ -386,7 +600,7 @@ public class PointerTracker {
return;
}
- if (!key.noKeyPreview()) {
+ if (!key.noKeyPreview() && !sInGesture && !needsToSuppressKeyPreviewPopup(eventTime)) {
mDrawingProxy.showKeyPreview(this);
}
updatePressKeyGraphics(key);
@@ -400,29 +614,33 @@ public class PointerTracker {
}
if (key.altCodeWhileTyping() && mTimerProxy.isTypingState()) {
- final int altCode = key.mAltCode;
+ final int altCode = key.getAltCode();
final Key altKey = mKeyboard.getKey(altCode);
if (altKey != null) {
updatePressKeyGraphics(altKey);
}
for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
- if (k != key && k.mAltCode == altCode) {
+ if (k != key && k.getAltCode() == altCode) {
updatePressKeyGraphics(k);
}
}
}
}
- private void updateReleaseKeyGraphics(Key key) {
+ private void updateReleaseKeyGraphics(final Key key) {
key.onReleased();
mDrawingProxy.invalidateKey(key);
}
- private void updatePressKeyGraphics(Key key) {
+ private void updatePressKeyGraphics(final Key key) {
key.onPressed();
mDrawingProxy.invalidateKey(key);
}
+ public GestureStrokeWithPreviewPoints getGestureStrokeWithPreviewPoints() {
+ return mGestureStrokeWithPreviewPoints;
+ }
+
public int getLastX() {
return mLastX;
}
@@ -435,30 +653,100 @@ public class PointerTracker {
return mDownTime;
}
- private Key onDownKey(int x, int y, long eventTime) {
+ private Key onDownKey(final int x, final int y, final long eventTime) {
mDownTime = eventTime;
+ mBogusMoveEventDetector.onDownKey();
return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
}
- private Key onMoveKeyInternal(int x, int y) {
+ static int getDistance(final int x1, final int y1, final int x2, final int y2) {
+ return (int)Math.hypot(x1 - x2, y1 - y2);
+ }
+
+ private Key onMoveKeyInternal(final int x, final int y) {
+ mBogusMoveEventDetector.onMoveKey(getDistance(x, y, mLastX, mLastY));
mLastX = x;
mLastY = y;
return mKeyDetector.detectHitKey(x, y);
}
- private Key onMoveKey(int x, int y) {
+ private Key onMoveKey(final int x, final int y) {
return onMoveKeyInternal(x, y);
}
- private Key onMoveToNewKey(Key newKey, int x, int y) {
+ private Key onMoveToNewKey(final Key newKey, final int x, final int y) {
mCurrentKey = newKey;
mKeyX = x;
mKeyY = y;
return newKey;
}
- public void processMotionEvent(int action, int x, int y, long eventTime,
- KeyEventHandler handler) {
+ private static int getActivePointerTrackerCount() {
+ return (sPointerTrackerQueue == null) ? 1 : sPointerTrackerQueue.size();
+ }
+
+ private void mayStartBatchInput(final Key key) {
+ if (sInGesture || !mGestureStrokeWithPreviewPoints.isStartOfAGesture()) {
+ return;
+ }
+ if (key == null || !Character.isLetter(key.mCode)) {
+ return;
+ }
+ if (DEBUG_LISTENER) {
+ Log.d(TAG, String.format("[%d] onStartBatchInput", mPointerId));
+ }
+ sInGesture = true;
+ synchronized (sAggregratedPointers) {
+ sAggregratedPointers.reset();
+ sLastRecognitionPointSize = 0;
+ sLastRecognitionTime = 0;
+ mListener.onStartBatchInput();
+ }
+ final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
+ mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
+ }
+
+ private void mayUpdateBatchInput(final long eventTime, final Key key) {
+ if (key != null) {
+ synchronized (sAggregratedPointers) {
+ final GestureStroke stroke = mGestureStrokeWithPreviewPoints;
+ stroke.appendIncrementalBatchPoints(sAggregratedPointers);
+ final int size = sAggregratedPointers.getPointerSize();
+ if (size > sLastRecognitionPointSize
+ && stroke.hasRecognitionTimePast(eventTime, sLastRecognitionTime)) {
+ sLastRecognitionPointSize = size;
+ sLastRecognitionTime = eventTime;
+ if (DEBUG_LISTENER) {
+ Log.d(TAG, String.format("[%d] onUpdateBatchInput: batchPoints=%d",
+ mPointerId, size));
+ }
+ mListener.onUpdateBatchInput(sAggregratedPointers);
+ }
+ }
+ }
+ final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
+ mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
+ }
+
+ private void mayEndBatchInput(final long eventTime) {
+ synchronized (sAggregratedPointers) {
+ mGestureStrokeWithPreviewPoints.appendAllBatchPoints(sAggregratedPointers);
+ if (getActivePointerTrackerCount() == 1) {
+ if (DEBUG_LISTENER) {
+ Log.d(TAG, String.format("[%d] onEndBatchInput : batchPoints=%d",
+ mPointerId, sAggregratedPointers.getPointerSize()));
+ }
+ sInGesture = false;
+ sTimeRecorder.onEndBatchInput(eventTime);
+ mListener.onEndBatchInput(sAggregratedPointers);
+ }
+ }
+ final boolean isOldestTracker = sPointerTrackerQueue.getOldestElement() == this;
+ mDrawingProxy.showGesturePreviewTrail(this, isOldestTracker);
+ }
+
+ public void processMotionEvent(final int action, final int x, final int y, final long eventTime,
+ final KeyEventHandler handler) {
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
@@ -469,7 +757,7 @@ public class PointerTracker {
onUpEvent(x, y, eventTime);
break;
case MotionEvent.ACTION_MOVE:
- onMoveEvent(x, y, eventTime);
+ onMoveEvent(x, y, eventTime, null);
break;
case MotionEvent.ACTION_CANCEL:
onCancelEvent(x, y, eventTime);
@@ -477,9 +765,11 @@ public class PointerTracker {
}
}
- public void onDownEvent(int x, int y, long eventTime, KeyEventHandler handler) {
- if (DEBUG_EVENT)
+ public 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();
@@ -488,24 +778,24 @@ public class PointerTracker {
// Naive up-to-down noise filter.
final long deltaT = eventTime - mUpTime;
if (deltaT < sParams.mTouchNoiseThresholdTime) {
- final int dx = x - mLastX;
- final int dy = y - mLastY;
- final int distanceSquared = (dx * dx + dy * dy);
- if (distanceSquared < sTouchNoiseThresholdDistanceSquared) {
+ final int distance = getDistance(x, y, mLastX, mLastY);
+ if (distance < sParams.mTouchNoiseThresholdDistance) {
if (DEBUG_MODE)
- Log.w(TAG, "onDownEvent: ignore potential noise: time=" + deltaT
- + " distance=" + distanceSquared);
+ Log.w(TAG, String.format("[%d] onDownEvent:"
+ + " ignore potential noise: time=%d distance=%d",
+ mPointerId, deltaT, distance));
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.pointerTracker_onDownEvent(deltaT, distanceSquared);
+ ResearchLogger.pointerTracker_onDownEvent(deltaT, distance * distance);
}
mKeyAlreadyProcessed = true;
return;
}
}
+ final Key key = getKeyOn(x, y);
+ mBogusMoveEventDetector.onActualDownEvent(x, y);
final PointerTrackerQueue queue = sPointerTrackerQueue;
if (queue != null) {
- final Key key = getKeyOn(x, y);
if (key != null && key.isModifier()) {
// Before processing a down event of modifier key, all pointers already being
// tracked should be released.
@@ -514,9 +804,22 @@ public class PointerTracker {
queue.add(this);
}
onDownEventInternal(x, y, eventTime);
+ if (!sShouldHandleGesture) {
+ return;
+ }
+ // A gesture should start only from the letter key.
+ mIsDetectingGesture = (mKeyboard != null) && mKeyboard.mId.isAlphabetKeyboard()
+ && !mIsShowingMoreKeysPanel && key != null && Keyboard.isLetterCode(key.mCode);
+ if (mIsDetectingGesture) {
+ if (getActivePointerTrackerCount() == 1) {
+ sGestureFirstDownTime = eventTime;
+ }
+ mGestureStrokeWithPreviewPoints.onDownEvent(x, y, eventTime, sGestureFirstDownTime,
+ sTimeRecorder.getLastLetterTypingTime());
+ }
}
- private void onDownEventInternal(int x, int y, long eventTime) {
+ private void onDownEventInternal(final int x, final int y, final long eventTime) {
Key key = onDownKey(x, y, eventTime);
// Sliding key is allowed when 1) enabled by configuration, 2) this pointer starts sliding
// from modifier key, or 3) this pointer's KeyDetector always allows sliding input.
@@ -525,9 +828,7 @@ public class PointerTracker {
|| mKeyDetector.alwaysAllowsSlidingInput();
mKeyboardLayoutHasBeenChanged = false;
mKeyAlreadyProcessed = false;
- mIsRepeatableKey = false;
- mIsInSlidingKeyInput = false;
- mIgnoreModifierKey = false;
+ resetSlidingKeyInput();
if (key != null) {
// This onPress call may have changed keyboard layout. Those cases are detected at
// {@link #setKeyboard}. In those cases, we should update key according to the new
@@ -538,27 +839,75 @@ public class PointerTracker {
startRepeatKey(key);
startLongPressTimer(key);
- setPressedKeyGraphics(key);
+ setPressedKeyGraphics(key, eventTime);
}
}
- private void startSlidingKeyInput(Key key) {
+ private void startSlidingKeyInput(final Key key) {
if (!mIsInSlidingKeyInput) {
- mIgnoreModifierKey = key.isModifier();
+ mIsInSlidingKeyInputFromModifier = key.isModifier();
}
mIsInSlidingKeyInput = true;
}
- public void onMoveEvent(int x, int y, long eventTime) {
- if (DEBUG_MOVE_EVENT)
+ private void resetSlidingKeyInput() {
+ mIsInSlidingKeyInput = false;
+ mIsInSlidingKeyInputFromModifier = false;
+ }
+
+ private void onGestureMoveEvent(final int x, final int y, final long eventTime,
+ final boolean isMajorEvent, final Key key) {
+ final int gestureTime = (int)(eventTime - sGestureFirstDownTime);
+ if (mIsDetectingGesture) {
+ mGestureStrokeWithPreviewPoints.addPoint(x, y, gestureTime, isMajorEvent);
+ mayStartBatchInput(key);
+ if (sInGesture) {
+ mayUpdateBatchInput(eventTime, key);
+ }
+ }
+ }
+
+ public void onMoveEvent(final int x, final int y, final long eventTime, final MotionEvent me) {
+ if (DEBUG_MOVE_EVENT) {
printTouchEvent("onMoveEvent:", x, y, eventTime);
- if (mKeyAlreadyProcessed)
+ }
+ if (mKeyAlreadyProcessed) {
return;
+ }
+
+ if (sShouldHandleGesture && me != null) {
+ // Add historical points to gesture path.
+ final int pointerIndex = me.findPointerIndex(mPointerId);
+ final int historicalSize = me.getHistorySize();
+ for (int h = 0; h < historicalSize; h++) {
+ final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
+ final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
+ final long historicalTime = me.getHistoricalEventTime(h);
+ onGestureMoveEvent(historicalX, historicalY, historicalTime,
+ false /* isMajorEvent */, null);
+ }
+ }
+ onMoveEventInternal(x, y, eventTime);
+ }
+
+ private void onMoveEventInternal(final int x, final int y, final long eventTime) {
final int lastX = mLastX;
final int lastY = mLastY;
final Key oldKey = mCurrentKey;
Key key = onMoveKey(x, y);
+
+ if (sShouldHandleGesture) {
+ // Register move event on gesture tracker.
+ onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, key);
+ if (sInGesture) {
+ mTimerProxy.cancelLongPressTimer();
+ mCurrentKey = null;
+ setReleasedKeyGraphics(oldKey);
+ return;
+ }
+ }
+
if (key != null) {
if (oldKey == null) {
// The pointer has been slid in to the new key, but the finger was not on any keys.
@@ -571,8 +920,8 @@ public class PointerTracker {
}
onMoveToNewKey(key, x, y);
startLongPressTimer(key);
- setPressedKeyGraphics(key);
- } else if (isMajorEnoughMoveToBeOnNewKey(x, y, key)) {
+ setPressedKeyGraphics(key, eventTime);
+ } else if (isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, key)) {
// The pointer has been slid in to the new key from the previous key, we must call
// onRelease() first to notify that the previous key has been released, then call
// onPress() to notify that the new key is being pressed.
@@ -590,34 +939,75 @@ public class PointerTracker {
}
onMoveToNewKey(key, x, y);
startLongPressTimer(key);
- setPressedKeyGraphics(key);
+ setPressedKeyGraphics(key, eventTime);
} else {
- // HACK: On some devices, quick successive touches may be translated to sudden
- // move by touch panel firmware. This hack detects the case and translates the
+ // HACK: On some devices, quick successive touches may be reported as a sudden
+ // move by touch panel firmware. This hack detects such cases and translates the
// move event to successive up and down events.
- final int dx = x - lastX;
- final int dy = y - lastY;
- final int lastMoveSquared = dx * dx + dy * dy;
+ // TODO: Should find a way to balance gesture detection and this hack.
if (sNeedsPhantomSuddenMoveEventHack
- && lastMoveSquared >= mKeyQuarterWidthSquared) {
+ && getDistance(x, y, lastX, lastY) >= mPhantonSuddenMoveThreshold) {
if (DEBUG_MODE) {
- Log.w(TAG, String.format("onMoveEvent:"
- + " phantom sudden move event is translated to "
- + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y));
+ Log.w(TAG, String.format("[%d] onMoveEvent:"
+ + " 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, Keyboard.printableCode(oldKey.mCode),
+ x, y, Keyboard.printableCode(key.mCode)));
}
+ // TODO: This should be moved to outside of this nested if-clause?
if (ProductionFlag.IS_EXPERIMENTAL) {
ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
}
- onUpEventInternal();
+ onUpEventInternal(eventTime);
+ onDownEventInternal(x, y, eventTime);
+ }
+ // HACK: On some devices, quick successive proximate touches may be reported as
+ // a bogus down-move-up event by touch panel firmware. This hack detects such
+ // cases and breaks these events into separate up and down events.
+ else if (sNeedsProximateBogusDownMoveUpEventHack
+ && sTimeRecorder.isInFastTyping(eventTime)
+ && mBogusMoveEventDetector.isCloseToActualDownEvent(x, y)) {
+ if (DEBUG_MODE) {
+ final float keyDiagonal = (float)Math.hypot(
+ mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
+ final float radiusRatio =
+ mBogusMoveEventDetector.getDistanceFromDownEvent(x, y)
+ / keyDiagonal;
+ Log.w(TAG, String.format("[%d] onMoveEvent:"
+ + " 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, Keyboard.printableCode(oldKey.mCode),
+ x, y, Keyboard.printableCode(key.mCode)));
+ }
+ onUpEventInternal(eventTime);
onDownEventInternal(x, y, eventTime);
} else {
- mKeyAlreadyProcessed = true;
+ // HACK: If there are currently multiple touches, register the key even if
+ // the finger slides off the key. This defends against noise from some
+ // touch panels when there are close multiple touches.
+ // Caveat: When in chording input mode with a modifier key, we don't use
+ // this hack.
+ if (getActivePointerTrackerCount() > 1 && sPointerTrackerQueue != null
+ && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
+ if (DEBUG_MODE) {
+ Log.w(TAG, String.format("[%d] onMoveEvent:"
+ + " detected sliding finger while multi touching",
+ mPointerId));
+ }
+ onUpEvent(x, y, eventTime);
+ mKeyAlreadyProcessed = true;
+ }
+ if (!mIsDetectingGesture) {
+ mKeyAlreadyProcessed = true;
+ }
setReleasedKeyGraphics(oldKey);
}
}
}
} else {
- if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, key)) {
+ if (oldKey != null && isMajorEnoughMoveToBeOnNewKey(x, y, eventTime, key)) {
// The pointer has been slid out from the previous key, we must call onRelease() to
// notify that the previous key has been released.
setReleasedKeyGraphics(oldKey);
@@ -627,60 +1017,82 @@ public class PointerTracker {
if (mIsAllowedSlidingKeyInput) {
onMoveToNewKey(key, x, y);
} else {
- mKeyAlreadyProcessed = true;
+ if (!mIsDetectingGesture) {
+ mKeyAlreadyProcessed = true;
+ }
}
}
}
}
- public void onUpEvent(int x, int y, long eventTime) {
- if (DEBUG_EVENT)
+ public void onUpEvent(final int x, final int y, final long eventTime) {
+ if (DEBUG_EVENT) {
printTouchEvent("onUpEvent :", x, y, eventTime);
+ }
final PointerTrackerQueue queue = sPointerTrackerQueue;
if (queue != null) {
- if (mCurrentKey != null && mCurrentKey.isModifier()) {
- // Before processing an up event of modifier key, all pointers already being
- // tracked should be released.
- queue.releaseAllPointersExcept(this, eventTime);
- } else {
- queue.releaseAllPointersOlderThan(this, eventTime);
+ if (!sInGesture) {
+ if (mCurrentKey != null && mCurrentKey.isModifier()) {
+ // Before processing an up event of modifier key, all pointers already being
+ // tracked should be released.
+ queue.releaseAllPointersExcept(this, eventTime);
+ } else {
+ queue.releaseAllPointersOlderThan(this, eventTime);
+ }
}
+ }
+ onUpEventInternal(eventTime);
+ if (queue != null) {
queue.remove(this);
}
- onUpEventInternal();
}
// Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
// This pointer tracker needs to keep the key top graphics "pressed", but needs to get a
// "virtual" up event.
- public void onPhantomUpEvent(int x, int y, long eventTime) {
- if (DEBUG_EVENT)
- printTouchEvent("onPhntEvent:", x, y, eventTime);
- onUpEventInternal();
+ @Override
+ public void onPhantomUpEvent(final long eventTime) {
+ if (DEBUG_EVENT) {
+ printTouchEvent("onPhntEvent:", getLastX(), getLastY(), eventTime);
+ }
+ onUpEventInternal(eventTime);
mKeyAlreadyProcessed = true;
}
- private void onUpEventInternal() {
+ private void onUpEventInternal(final long eventTime) {
mTimerProxy.cancelKeyTimers();
- mIsInSlidingKeyInput = false;
+ resetSlidingKeyInput();
+ mIsDetectingGesture = false;
+ final Key currentKey = mCurrentKey;
+ mCurrentKey = null;
// Release the last pressed key.
- setReleasedKeyGraphics(mCurrentKey);
+ setReleasedKeyGraphics(currentKey);
if (mIsShowingMoreKeysPanel) {
mDrawingProxy.dismissMoreKeysPanel();
mIsShowingMoreKeysPanel = false;
}
- if (mKeyAlreadyProcessed)
+
+ if (sInGesture) {
+ if (currentKey != null) {
+ callListenerOnRelease(currentKey, currentKey.mCode, true);
+ }
+ mayEndBatchInput(eventTime);
+ return;
+ }
+
+ if (mKeyAlreadyProcessed) {
return;
- if (!mIsRepeatableKey) {
- detectAndSendKey(mCurrentKey, mKeyX, mKeyY);
+ }
+ if (currentKey != null && !currentKey.isRepeatable()) {
+ detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
}
}
- public void onShowMoreKeysPanel(int x, int y, KeyEventHandler handler) {
+ public void onShowMoreKeysPanel(final int x, final int y, final KeyEventHandler handler) {
onLongPressed();
- onDownEvent(x, y, SystemClock.uptimeMillis(), handler);
mIsShowingMoreKeysPanel = true;
+ onDownEvent(x, y, SystemClock.uptimeMillis(), handler);
}
public void onLongPressed() {
@@ -692,9 +1104,10 @@ public class PointerTracker {
}
}
- public void onCancelEvent(int x, int y, long eventTime) {
- if (DEBUG_EVENT)
+ public void onCancelEvent(final int x, final int y, final long eventTime) {
+ if (DEBUG_EVENT) {
printTouchEvent("onCancelEvt:", x, y, eventTime);
+ }
final PointerTrackerQueue queue = sPointerTrackerQueue;
if (queue != null) {
@@ -707,71 +1120,91 @@ public class PointerTracker {
private void onCancelEventInternal() {
mTimerProxy.cancelKeyTimers();
setReleasedKeyGraphics(mCurrentKey);
- mIsInSlidingKeyInput = false;
+ resetSlidingKeyInput();
if (mIsShowingMoreKeysPanel) {
mDrawingProxy.dismissMoreKeysPanel();
mIsShowingMoreKeysPanel = false;
}
}
- private void startRepeatKey(Key key) {
- if (key != null && key.isRepeatable()) {
+ private void startRepeatKey(final Key key) {
+ if (key != null && key.isRepeatable() && !sInGesture) {
onRegisterKey(key);
mTimerProxy.startKeyRepeatTimer(this);
- mIsRepeatableKey = true;
- } else {
- mIsRepeatableKey = false;
}
}
- public void onRegisterKey(Key key) {
+ public void onRegisterKey(final Key key) {
if (key != null) {
- detectAndSendKey(key, key.mX, key.mY);
- if (!key.altCodeWhileTyping() && !key.isModifier()) {
- mTimerProxy.startTypingStateTimer();
- }
+ detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
+ mTimerProxy.startTypingStateTimer(key);
}
}
- private boolean isMajorEnoughMoveToBeOnNewKey(int x, int y, Key newKey) {
- if (mKeyDetector == null)
+ private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime,
+ final Key newKey) {
+ if (mKeyDetector == null) {
throw new NullPointerException("keyboard and/or key detector not set");
- Key curKey = mCurrentKey;
+ }
+ final Key curKey = mCurrentKey;
if (newKey == curKey) {
return false;
} else if (curKey != null) {
- return curKey.squaredDistanceToEdge(x, y)
- >= mKeyDetector.getKeyHysteresisDistanceSquared();
- } else {
+ final int keyHysteresisDistanceSquared = mKeyDetector.getKeyHysteresisDistanceSquared(
+ mIsInSlidingKeyInputFromModifier);
+ final int distanceFromKeyEdgeSquared = curKey.squaredDistanceToEdge(x, y);
+ if (distanceFromKeyEdgeSquared >= keyHysteresisDistanceSquared) {
+ if (DEBUG_MODE) {
+ final float distanceToEdgeRatio = (float)Math.sqrt(distanceFromKeyEdgeSquared)
+ / mKeyboard.mMostCommonKeyWidth;
+ Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
+ +" %.2f key width from key edge",
+ mPointerId, distanceToEdgeRatio));
+ }
+ return true;
+ }
+ if (sNeedsProximateBogusDownMoveUpEventHack && !mIsAllowedSlidingKeyInput
+ && sTimeRecorder.isInFastTyping(eventTime)
+ && mBogusMoveEventDetector.hasTraveledLongDistance(x, y)) {
+ if (DEBUG_MODE) {
+ final float keyDiagonal = (float)Math.hypot(
+ mKeyboard.mMostCommonKeyWidth, mKeyboard.mMostCommonKeyHeight);
+ final float lengthFromDownRatio =
+ mBogusMoveEventDetector.mAccumulatedDistanceFromDownKey / keyDiagonal;
+ Log.d(TAG, String.format("[%d] isMajorEnoughMoveToBeOnNewKey:"
+ + " %.2f key diagonal from virtual down point",
+ mPointerId, lengthFromDownRatio));
+ }
+ return true;
+ }
+ return false;
+ } else { // curKey == null && newKey != null
return true;
}
}
- private void startLongPressTimer(Key key) {
- if (key != null && key.isLongPressEnabled()) {
+ private void startLongPressTimer(final Key key) {
+ if (key != null && key.isLongPressEnabled() && !sInGesture) {
mTimerProxy.startLongPressTimer(this);
}
}
- private void detectAndSendKey(Key key, int x, int y) {
+ private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) {
if (key == null) {
callListenerOnCancelInput();
return;
}
- int code = key.mCode;
- callListenerOnCodeInput(key, code, x, y);
+ final int code = key.mCode;
+ callListenerOnCodeInput(key, code, x, y, eventTime);
callListenerOnRelease(key, code, false);
}
- private long mPreviousEventTime;
-
- private void printTouchEvent(String title, int x, int y, long eventTime) {
+ private void printTouchEvent(final String title, final int x, final int y,
+ final long eventTime) {
final Key key = mKeyDetector.detectHitKey(x, y);
final String code = KeyDetector.printableCode(key);
- final long delta = eventTime - mPreviousEventTime;
- Log.d(TAG, String.format("%s%s[%d] %4d %4d %5d %s", title,
- (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, delta, code));
- mPreviousEventTime = eventTime;
+ Log.d(TAG, String.format("[%d]%s%s %4d %4d %5d %s", mPointerId,
+ (mKeyAlreadyProcessed ? "-" : " "), title, x, y, eventTime, code));
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index 1207c3fcd..06a9e9252 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -18,21 +18,22 @@ package com.android.inputmethod.keyboard;
import android.graphics.Rect;
import android.text.TextUtils;
-import android.util.FloatMath;
-import com.android.inputmethod.keyboard.Keyboard.Params.TouchPositionCorrection;
+import com.android.inputmethod.keyboard.internal.TouchPositionCorrection;
+import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.JniUtils;
import java.util.Arrays;
-import java.util.HashMap;
-public class ProximityInfo {
+public final class ProximityInfo {
+ /** MAX_PROXIMITY_CHARS_SIZE must be the same as MAX_PROXIMITY_CHARS_SIZE_INTERNAL
+ * in defines.h */
public static final int MAX_PROXIMITY_CHARS_SIZE = 16;
/** Number of key widths from current touch point to search for nearest keys. */
private static float SEARCH_DISTANCE = 1.2f;
private static final Key[] EMPTY_KEY_ARRAY = new Key[0];
+ private static final float DEFAULT_TOUCH_POSITION_CORRECTION_RADIUS = 0.15f;
- private final int mKeyHeight;
private final int mGridWidth;
private final int mGridHeight;
private final int mGridSize;
@@ -42,14 +43,15 @@ public class ProximityInfo {
private final int mKeyboardMinWidth;
private final int mKeyboardHeight;
private final int mMostCommonKeyWidth;
+ private final int mMostCommonKeyHeight;
private final Key[] mKeys;
- private final TouchPositionCorrection mTouchPositionCorrection;
private final Key[][] mGridNeighbors;
private final String mLocaleStr;
- ProximityInfo(String localeStr, int gridWidth, int gridHeight, int minWidth, int height,
- int mostCommonKeyWidth, int mostCommonKeyHeight, final Key[] keys,
- TouchPositionCorrection touchPositionCorrection) {
+ ProximityInfo(final String localeStr, final int gridWidth, final int gridHeight,
+ final int minWidth, final int height, final int mostCommonKeyWidth,
+ final int mostCommonKeyHeight, final Key[] keys,
+ final TouchPositionCorrection touchPositionCorrection) {
if (TextUtils.isEmpty(localeStr)) {
mLocaleStr = "";
} else {
@@ -62,38 +64,16 @@ public class ProximityInfo {
mCellHeight = (height + mGridHeight - 1) / mGridHeight;
mKeyboardMinWidth = minWidth;
mKeyboardHeight = height;
- mKeyHeight = mostCommonKeyHeight;
+ mMostCommonKeyHeight = mostCommonKeyHeight;
mMostCommonKeyWidth = mostCommonKeyWidth;
mKeys = keys;
- mTouchPositionCorrection = touchPositionCorrection;
mGridNeighbors = new Key[mGridSize][];
if (minWidth == 0 || height == 0) {
// No proximity required. Keyboard might be more keys keyboard.
return;
}
computeNearestNeighbors();
- mNativeProximityInfo = createNativeProximityInfo();
- }
-
- // TODO: Remove this public constructor when the native part of the ProximityInfo becomes
- // immutable.
- // This public constructor aims only for test purpose.
- public ProximityInfo(ProximityInfo o) {
- mLocaleStr = o.mLocaleStr;
- mGridWidth = o.mGridWidth;
- mGridHeight = o.mGridHeight;
- mGridSize = o.mGridSize;
- mCellWidth = o.mCellWidth;
- mCellHeight = o.mCellHeight;
- mKeyboardMinWidth = o.mKeyboardMinWidth;
- mKeyboardHeight = o.mKeyboardHeight;
- mKeyHeight = o.mKeyHeight;
- mMostCommonKeyWidth = o.mMostCommonKeyWidth;
- mKeys = o.mKeys;
- mTouchPositionCorrection = o.mTouchPositionCorrection;
- mGridNeighbors = new Key[mGridSize][];
- computeNearestNeighbors();
- mNativeProximityInfo = createNativeProximityInfo();
+ mNativeProximityInfo = createNativeProximityInfo(touchPositionCorrection);
}
public static ProximityInfo createDummyProximityInfo() {
@@ -101,7 +81,7 @@ public class ProximityInfo {
}
public static ProximityInfo createSpellCheckerProximityInfo(final int[] proximity,
- int rowSize, int gridWidth, int gridHeight) {
+ final int rowSize, final int gridWidth, final int gridHeight) {
final ProximityInfo spellCheckerProximityInfo = createDummyProximityInfo();
spellCheckerProximityInfo.mNativeProximityInfo =
spellCheckerProximityInfo.setProximityInfoNative("",
@@ -125,14 +105,14 @@ public class ProximityInfo {
private native void releaseProximityInfoNative(long nativeProximityInfo);
- private final long createNativeProximityInfo() {
+ private final long createNativeProximityInfo(
+ final TouchPositionCorrection touchPositionCorrection) {
final Key[][] gridNeighborKeys = mGridNeighbors;
final int keyboardWidth = mKeyboardMinWidth;
final int keyboardHeight = mKeyboardHeight;
final Key[] keys = mKeys;
- final TouchPositionCorrection touchPositionCorrection = mTouchPositionCorrection;
final int[] proximityCharsArray = new int[mGridSize * MAX_PROXIMITY_CHARS_SIZE];
- Arrays.fill(proximityCharsArray, KeyDetector.NOT_A_CODE);
+ Arrays.fill(proximityCharsArray, Constants.NOT_A_CODE);
for (int i = 0; i < mGridSize; ++i) {
final int proximityCharsLength = gridNeighborKeys[i].length;
for (int j = 0; j < proximityCharsLength; ++j) {
@@ -163,20 +143,22 @@ public class ProximityInfo {
sweetSpotCenterXs = new float[keyCount];
sweetSpotCenterYs = new float[keyCount];
sweetSpotRadii = new float[keyCount];
+ final float defaultRadius = DEFAULT_TOUCH_POSITION_CORRECTION_RADIUS
+ * (float)Math.hypot(mMostCommonKeyWidth, mMostCommonKeyHeight);
for (int i = 0; i < keyCount; i++) {
final Key key = keys[i];
final Rect hitBox = key.mHitBox;
- final int row = hitBox.top / mKeyHeight;
- if (row < touchPositionCorrection.mRadii.length) {
+ sweetSpotCenterXs[i] = hitBox.exactCenterX();
+ sweetSpotCenterYs[i] = hitBox.exactCenterY();
+ sweetSpotRadii[i] = defaultRadius;
+ final int row = hitBox.top / mMostCommonKeyHeight;
+ if (row < touchPositionCorrection.getRows()) {
final int hitBoxWidth = hitBox.width();
final int hitBoxHeight = hitBox.height();
- final float x = touchPositionCorrection.mXs[row];
- final float y = touchPositionCorrection.mYs[row];
- final float radius = touchPositionCorrection.mRadii[row];
- sweetSpotCenterXs[i] = hitBox.exactCenterX() + x * hitBoxWidth;
- sweetSpotCenterYs[i] = hitBox.exactCenterY() + y * hitBoxHeight;
- sweetSpotRadii[i] = radius * FloatMath.sqrt(
- hitBoxWidth * hitBoxWidth + hitBoxHeight * hitBoxHeight);
+ final float hitBoxDiagonal = (float)Math.hypot(hitBoxWidth, hitBoxHeight);
+ sweetSpotCenterXs[i] += touchPositionCorrection.getX(row) * hitBoxWidth;
+ sweetSpotCenterYs[i] += touchPositionCorrection.getY(row) * hitBoxHeight;
+ sweetSpotRadii[i] = touchPositionCorrection.getRadius(row) * hitBoxDiagonal;
}
}
} else {
@@ -209,10 +191,6 @@ public class ProximityInfo {
private void computeNearestNeighbors() {
final int defaultWidth = mMostCommonKeyWidth;
final Key[] keys = mKeys;
- final HashMap<Integer, Key> keyCodeMap = new HashMap<Integer, Key>();
- for (final Key key : keys) {
- keyCodeMap.put(key.mCode, key);
- }
final int thresholdBase = (int) (defaultWidth * SEARCH_DISTANCE);
final int threshold = thresholdBase * thresholdBase;
// Round-up so we don't have any pixels outside the grid
@@ -236,7 +214,8 @@ public class ProximityInfo {
}
}
- public void fillArrayWithNearestKeyCodes(int x, int y, int primaryKeyCode, int[] dest) {
+ public void fillArrayWithNearestKeyCodes(final int x, final int y, final int primaryKeyCode,
+ final int[] dest) {
final int destLength = dest.length;
if (destLength < 1) {
return;
@@ -257,11 +236,11 @@ public class ProximityInfo {
dest[index++] = code;
}
if (index < destLength) {
- dest[index] = KeyDetector.NOT_A_CODE;
+ dest[index] = Constants.NOT_A_CODE;
}
}
- public Key[] getNearestKeys(int x, int y) {
+ public Key[] getNearestKeys(final int x, final int y) {
if (mGridNeighbors == null) {
return EMPTY_KEY_ARRAY;
}
diff --git a/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java b/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java
index ee5047083..dc12fa468 100644
--- a/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java
+++ b/java/src/com/android/inputmethod/keyboard/ViewLayoutUtils.java
@@ -22,7 +22,7 @@ import android.view.ViewGroup.MarginLayoutParams;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
-public class ViewLayoutUtils {
+public final class ViewLayoutUtils {
private ViewLayoutUtils() {
// This utility class is not publicly instantiable.
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/AlphabetShiftState.java b/java/src/com/android/inputmethod/keyboard/internal/AlphabetShiftState.java
index 5712df1fc..44aa72a0a 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/AlphabetShiftState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/AlphabetShiftState.java
@@ -18,7 +18,7 @@ package com.android.inputmethod.keyboard.internal;
import android.util.Log;
-public class AlphabetShiftState {
+public final class AlphabetShiftState {
private static final String TAG = AlphabetShiftState.class.getSimpleName();
private static final boolean DEBUG = false;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
new file mode 100644
index 000000000..699aaeaef
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
@@ -0,0 +1,297 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.SystemClock;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResizableIntArray;
+
+final class GesturePreviewTrail {
+ private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY;
+
+ private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
+ private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
+ private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
+ private int mCurrentStrokeId = -1;
+ // The wall time of the zero value in {@link #mEventTimes}
+ private long mCurrentTimeBase;
+ private int mTrailStartIndex;
+
+ static final class Params {
+ public final int mTrailColor;
+ public final float mTrailStartWidth;
+ public final float mTrailEndWidth;
+ public final int mFadeoutStartDelay;
+ public final int mFadeoutDuration;
+ public final int mUpdateInterval;
+
+ public final int mTrailLingerDuration;
+
+ public Params(final TypedArray keyboardViewAttr) {
+ mTrailColor = keyboardViewAttr.getColor(
+ R.styleable.KeyboardView_gesturePreviewTrailColor, 0);
+ mTrailStartWidth = keyboardViewAttr.getDimension(
+ R.styleable.KeyboardView_gesturePreviewTrailStartWidth, 0.0f);
+ mTrailEndWidth = keyboardViewAttr.getDimension(
+ R.styleable.KeyboardView_gesturePreviewTrailEndWidth, 0.0f);
+ mFadeoutStartDelay = keyboardViewAttr.getInt(
+ R.styleable.KeyboardView_gesturePreviewTrailFadeoutStartDelay, 0);
+ mFadeoutDuration = keyboardViewAttr.getInt(
+ R.styleable.KeyboardView_gesturePreviewTrailFadeoutDuration, 0);
+ mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration;
+ mUpdateInterval = keyboardViewAttr.getInt(
+ R.styleable.KeyboardView_gesturePreviewTrailUpdateInterval, 0);
+ }
+ }
+
+ // Use this value as imaginary zero because x-coordinates may be zero.
+ private static final int DOWN_EVENT_MARKER = -128;
+
+ private static int markAsDownEvent(final int xCoord) {
+ return DOWN_EVENT_MARKER - xCoord;
+ }
+
+ private static boolean isDownEventXCoord(final int xCoordOrMark) {
+ return xCoordOrMark <= DOWN_EVENT_MARKER;
+ }
+
+ private static int getXCoordValue(final int xCoordOrMark) {
+ return isDownEventXCoord(xCoordOrMark)
+ ? DOWN_EVENT_MARKER - xCoordOrMark : xCoordOrMark;
+ }
+
+ public void addStroke(final GestureStrokeWithPreviewPoints stroke, final long downTime) {
+ final int trailSize = mEventTimes.getLength();
+ stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates);
+ if (mEventTimes.getLength() == trailSize) {
+ return;
+ }
+ final int[] eventTimes = mEventTimes.getPrimitiveArray();
+ final int strokeId = stroke.getGestureStrokeId();
+ if (strokeId != mCurrentStrokeId) {
+ final int elapsedTime = (int)(downTime - mCurrentTimeBase);
+ for (int i = mTrailStartIndex; i < trailSize; i++) {
+ // Decay the previous strokes' event times.
+ eventTimes[i] -= elapsedTime;
+ }
+ final int[] xCoords = mXCoordinates.getPrimitiveArray();
+ final int downIndex = trailSize;
+ xCoords[downIndex] = markAsDownEvent(xCoords[downIndex]);
+ mCurrentTimeBase = downTime - eventTimes[downIndex];
+ mCurrentStrokeId = strokeId;
+ }
+ }
+
+ private static int getAlpha(final int elapsedTime, final Params params) {
+ if (elapsedTime < params.mFadeoutStartDelay) {
+ return Constants.Color.ALPHA_OPAQUE;
+ }
+ final int decreasingAlpha = Constants.Color.ALPHA_OPAQUE
+ * (elapsedTime - params.mFadeoutStartDelay)
+ / params.mFadeoutDuration;
+ return Constants.Color.ALPHA_OPAQUE - decreasingAlpha;
+ }
+
+ private static float getWidth(final int elapsedTime, final Params params) {
+ return Math.max((params.mTrailLingerDuration - elapsedTime)
+ * (params.mTrailStartWidth - params.mTrailEndWidth)
+ / params.mTrailLingerDuration, 0.0f);
+ }
+
+ static final class WorkingSet {
+ // Input
+ // Previous point (P1) coordinates and trail radius.
+ public float p1x, p1y;
+ public float r1;
+ // Current point (P2) coordinates and trail radius.
+ public float p2x, p2y;
+ public float r2;
+
+ // Output
+ // Closing point of arc at P1.
+ public float p1ax, p1ay;
+ // Opening point of arc at P1.
+ public float p1bx, p1by;
+ // Opening point of arc at P2.
+ public float p2ax, p2ay;
+ // Closing point of arc at P2.
+ public float p2bx, p2by;
+ // Start angle of the trail arcs.
+ public float aa;
+ // Sweep angle of the trail arc at P1.
+ public float a1;
+ public RectF arc1 = new RectF();
+ // Sweep angle of the trail arc at P2.
+ public float a2;
+ public RectF arc2 = new RectF();
+ }
+
+ private static final float RIGHT_ANGLE = (float)(Math.PI / 2.0d);
+ private static final float RADIAN_TO_DEGREE = (float)(180.0d / Math.PI);
+
+ private static boolean calculatePathPoints(final WorkingSet w) {
+ final float dx = w.p2x - w.p1x;
+ final float dy = w.p2y - w.p1y;
+ // Distance of the points.
+ final double l = Math.hypot(dx, dy);
+ if (Double.compare(0.0d, l) == 0) {
+ return false;
+ }
+ // Angle of the line p1-p2
+ final float a = (float)Math.atan2(dy, dx);
+ // Difference of trail cap radius.
+ final float dr = w.r2 - w.r1;
+ // Variation of angle at trail cap.
+ final float ar = (float)Math.asin(dr / l);
+ // The start angle of trail cap arc at P1.
+ final float aa = a - (RIGHT_ANGLE + ar);
+ // The end angle of trail cap arc at P2.
+ final float ab = a + (RIGHT_ANGLE + ar);
+ final float cosa = (float)Math.cos(aa);
+ final float sina = (float)Math.sin(aa);
+ final float cosb = (float)Math.cos(ab);
+ final float sinb = (float)Math.sin(ab);
+ w.p1ax = w.p1x + w.r1 * cosa;
+ w.p1ay = w.p1y + w.r1 * sina;
+ w.p1bx = w.p1x + w.r1 * cosb;
+ w.p1by = w.p1y + w.r1 * sinb;
+ w.p2ax = w.p2x + w.r2 * cosa;
+ w.p2ay = w.p2y + w.r2 * sina;
+ w.p2bx = w.p2x + w.r2 * cosb;
+ w.p2by = w.p2y + w.r2 * sinb;
+ w.aa = aa * RADIAN_TO_DEGREE;
+ final float ar2degree = ar * 2.0f * RADIAN_TO_DEGREE;
+ w.a1 = -180.0f + ar2degree;
+ w.a2 = 180.0f + ar2degree;
+ w.arc1.set(w.p1x, w.p1y, w.p1x, w.p1y);
+ w.arc1.inset(-w.r1, -w.r1);
+ w.arc2.set(w.p2x, w.p2y, w.p2x, w.p2y);
+ w.arc2.inset(-w.r2, -w.r2);
+ return true;
+ }
+
+ private static void createPath(final Path path, final WorkingSet w) {
+ path.rewind();
+ // Trail cap at P1.
+ path.moveTo(w.p1x, w.p1y);
+ path.arcTo(w.arc1, w.aa, w.a1);
+ // Trail cap at P2.
+ path.moveTo(w.p2x, w.p2y);
+ path.arcTo(w.arc2, w.aa, w.a2);
+ // Two trapezoids connecting P1 and P2.
+ path.moveTo(w.p1ax, w.p1ay);
+ path.lineTo(w.p1x, w.p1y);
+ path.lineTo(w.p1bx, w.p1by);
+ path.lineTo(w.p2bx, w.p2by);
+ path.lineTo(w.p2x, w.p2y);
+ path.lineTo(w.p2ax, w.p2ay);
+ path.close();
+ }
+
+ private final WorkingSet mWorkingSet = new WorkingSet();
+ private final Path mPath = new Path();
+
+ /**
+ * Draw gesture preview trail
+ * @param canvas The canvas to draw the gesture preview trail
+ * @param paint The paint object to be used to draw the gesture preview trail
+ * @param outBoundsRect the bounding box of this gesture trail drawing
+ * @param params The drawing parameters of gesture preview trail
+ * @return true if some gesture preview trails remain to be drawn
+ */
+ public boolean drawGestureTrail(final Canvas canvas, final Paint paint,
+ final Rect outBoundsRect, final Params params) {
+ final int trailSize = mEventTimes.getLength();
+ if (trailSize == 0) {
+ return false;
+ }
+
+ final int[] eventTimes = mEventTimes.getPrimitiveArray();
+ final int[] xCoords = mXCoordinates.getPrimitiveArray();
+ final int[] yCoords = mYCoordinates.getPrimitiveArray();
+ final int sinceDown = (int)(SystemClock.uptimeMillis() - mCurrentTimeBase);
+ int startIndex;
+ for (startIndex = mTrailStartIndex; startIndex < trailSize; startIndex++) {
+ final int elapsedTime = sinceDown - eventTimes[startIndex];
+ // Skip too old trail points.
+ if (elapsedTime < params.mTrailLingerDuration) {
+ break;
+ }
+ }
+ mTrailStartIndex = startIndex;
+
+ if (startIndex < trailSize) {
+ paint.setColor(params.mTrailColor);
+ paint.setStyle(Paint.Style.FILL);
+ final Path path = mPath;
+ final WorkingSet w = mWorkingSet;
+ w.p1x = getXCoordValue(xCoords[startIndex]);
+ w.p1y = yCoords[startIndex];
+ int lastTime = sinceDown - eventTimes[startIndex];
+ float maxWidth = getWidth(lastTime, params);
+ w.r1 = maxWidth / 2.0f;
+ // Initialize bounds rectangle.
+ outBoundsRect.set((int)w.p1x, (int)w.p1y, (int)w.p1x, (int)w.p1y);
+ for (int i = startIndex + 1; i < trailSize - 1; i++) {
+ final int elapsedTime = sinceDown - eventTimes[i];
+ w.p2x = getXCoordValue(xCoords[i]);
+ w.p2y = yCoords[i];
+ // Draw trail line only when the current point isn't a down point.
+ if (!isDownEventXCoord(xCoords[i])) {
+ final int alpha = getAlpha(elapsedTime, params);
+ paint.setAlpha(alpha);
+ final float width = getWidth(elapsedTime, params);
+ w.r2 = width / 2.0f;
+ if (calculatePathPoints(w)) {
+ createPath(path, w);
+ canvas.drawPath(path, paint);
+ outBoundsRect.union((int)w.p2x, (int)w.p2y);
+ }
+ // Take union for the bounds.
+ maxWidth = Math.max(maxWidth, width);
+ }
+ w.p1x = w.p2x;
+ w.p1y = w.p2y;
+ w.r1 = w.r2;
+ lastTime = elapsedTime;
+ }
+ // Take care of trail line width.
+ final int inset = -((int)maxWidth + 1);
+ outBoundsRect.inset(inset, inset);
+ }
+
+ final int newSize = trailSize - startIndex;
+ if (newSize < startIndex) {
+ mTrailStartIndex = 0;
+ if (newSize > 0) {
+ System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize);
+ System.arraycopy(xCoords, startIndex, xCoords, 0, newSize);
+ System.arraycopy(yCoords, startIndex, yCoords, 0, newSize);
+ }
+ mEventTimes.setLength(newSize);
+ mXCoordinates.setLength(newSize);
+ mYCoordinates.setLength(newSize);
+ }
+ return newSize > 0;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
new file mode 100644
index 000000000..f8244dd5b
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
@@ -0,0 +1,341 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.res.TypedArray;
+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;
+
+public class GestureStroke {
+ private static final String TAG = GestureStroke.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ private static final boolean DEBUG_SPEED = false;
+
+ public static final int DEFAULT_CAPACITY = 128;
+
+ private final int mPointerId;
+ private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
+ private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
+ private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
+
+ private final GestureStrokeParams mParams;
+
+ private int mKeyWidth; // pixel
+ // Static threshold for starting gesture detection
+ private int mDetectFastMoveSpeedThreshold; // pixel /sec
+ private int mDetectFastMoveTime;
+ private int mDetectFastMoveX;
+ private int mDetectFastMoveY;
+ // Dynamic threshold for gesture after fast typing
+ private boolean mAfterFastTyping;
+ private int mGestureDynamicDistanceThresholdFrom; // pixel
+ private int mGestureDynamicDistanceThresholdTo; // pixel
+ // Variables for gesture sampling
+ private int mGestureSamplingMinimumDistance; // pixel
+ private long mLastMajorEventTime;
+ private int mLastMajorEventX;
+ private int mLastMajorEventY;
+ // Variables for gesture recognition
+ private int mGestureRecognitionSpeedThreshold; // pixel / sec
+ private int mIncrementalRecognitionSize;
+ private int mLastIncrementalBatchSize;
+
+ public static final class GestureStrokeParams {
+ // Static threshold for gesture after fast typing
+ public final int mStaticTimeThresholdAfterFastTyping; // msec
+ // Static threshold for starting gesture detection
+ public final float mDetectFastMoveSpeedThreshold; // keyWidth/sec
+ // Dynamic threshold for gesture after fast typing
+ public final int mDynamicThresholdDecayDuration; // msec
+ // Time based threshold values
+ public final int mDynamicTimeThresholdFrom; // msec
+ public final int mDynamicTimeThresholdTo; // msec
+ // Distance based threshold values
+ public final float mDynamicDistanceThresholdFrom; // keyWidth
+ public final float mDynamicDistanceThresholdTo; // keyWidth
+ // Parameters for gesture sampling
+ public final float mSamplingMinimumDistance; // keyWidth
+ // Parameters for gesture recognition
+ public final int mRecognitionMinimumTime; // msec
+ public final float mRecognitionSpeedThreshold; // keyWidth/sec
+
+ // Default GestureStroke parameters for test.
+ public static final GestureStrokeParams FOR_TEST = new GestureStrokeParams();
+ public static final GestureStrokeParams DEFAULT = FOR_TEST;
+
+ private GestureStrokeParams() {
+ // These parameter values are default and intended for testing.
+ mStaticTimeThresholdAfterFastTyping = 350; // msec
+ mDetectFastMoveSpeedThreshold = 1.5f; // keyWidth / sec
+ mDynamicThresholdDecayDuration = 450; // msec
+ mDynamicTimeThresholdFrom = 300; // msec
+ mDynamicTimeThresholdTo = 20; // msec
+ mDynamicDistanceThresholdFrom = 6.0f; // keyWidth
+ mDynamicDistanceThresholdTo = 0.35f; // keyWidth
+ // The following parameters' change will affect the result of regression test.
+ mSamplingMinimumDistance = 1.0f / 6.0f; // keyWidth
+ mRecognitionMinimumTime = 100; // msec
+ mRecognitionSpeedThreshold = 5.5f; // keyWidth / sec
+ }
+
+ public GestureStrokeParams(final TypedArray mainKeyboardViewAttr) {
+ mStaticTimeThresholdAfterFastTyping = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping,
+ DEFAULT.mStaticTimeThresholdAfterFastTyping);
+ mDetectFastMoveSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr,
+ R.styleable.MainKeyboardView_gestureDetectFastMoveSpeedThreshold,
+ DEFAULT.mDetectFastMoveSpeedThreshold);
+ mDynamicThresholdDecayDuration = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_gestureDynamicThresholdDecayDuration,
+ DEFAULT.mDynamicThresholdDecayDuration);
+ mDynamicTimeThresholdFrom = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_gestureDynamicTimeThresholdFrom,
+ DEFAULT.mDynamicTimeThresholdFrom);
+ mDynamicTimeThresholdTo = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_gestureDynamicTimeThresholdTo,
+ DEFAULT.mDynamicTimeThresholdTo);
+ mDynamicDistanceThresholdFrom = ResourceUtils.getFraction(mainKeyboardViewAttr,
+ R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdFrom,
+ DEFAULT.mDynamicDistanceThresholdFrom);
+ mDynamicDistanceThresholdTo = ResourceUtils.getFraction(mainKeyboardViewAttr,
+ R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdTo,
+ DEFAULT.mDynamicDistanceThresholdTo);
+ mSamplingMinimumDistance = ResourceUtils.getFraction(mainKeyboardViewAttr,
+ R.styleable.MainKeyboardView_gestureSamplingMinimumDistance,
+ DEFAULT.mSamplingMinimumDistance);
+ mRecognitionMinimumTime = mainKeyboardViewAttr.getInt(
+ R.styleable.MainKeyboardView_gestureRecognitionMinimumTime,
+ DEFAULT.mRecognitionMinimumTime);
+ mRecognitionSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr,
+ R.styleable.MainKeyboardView_gestureRecognitionSpeedThreshold,
+ DEFAULT.mRecognitionSpeedThreshold);
+ }
+ }
+
+ private static final int MSEC_PER_SEC = 1000;
+
+ public GestureStroke(final int pointerId, final GestureStrokeParams params) {
+ mPointerId = pointerId;
+ mParams = params;
+ }
+
+ public void setKeyboardGeometry(final int keyWidth) {
+ mKeyWidth = keyWidth;
+ // TODO: Find an appropriate base metric for these length. Maybe diagonal length of the key?
+ mDetectFastMoveSpeedThreshold = (int)(keyWidth * mParams.mDetectFastMoveSpeedThreshold);
+ mGestureDynamicDistanceThresholdFrom =
+ (int)(keyWidth * mParams.mDynamicDistanceThresholdFrom);
+ mGestureDynamicDistanceThresholdTo = (int)(keyWidth * mParams.mDynamicDistanceThresholdTo);
+ mGestureSamplingMinimumDistance = (int)(keyWidth * mParams.mSamplingMinimumDistance);
+ mGestureRecognitionSpeedThreshold = (int)(keyWidth * mParams.mRecognitionSpeedThreshold);
+ if (DEBUG) {
+ Log.d(TAG, String.format(
+ "[%d] setKeyboardGeometry: keyWidth=%3d tT=%3d >> %3d tD=%3d >> %3d",
+ mPointerId, keyWidth,
+ mParams.mDynamicTimeThresholdFrom,
+ mParams.mDynamicTimeThresholdTo,
+ mGestureDynamicDistanceThresholdFrom,
+ mGestureDynamicDistanceThresholdTo));
+ }
+ }
+
+ public void onDownEvent(final int x, final int y, final long downTime,
+ final long gestureFirstDownTime, final long lastTypingTime) {
+ reset();
+ final long elapsedTimeAfterTyping = downTime - lastTypingTime;
+ if (elapsedTimeAfterTyping < mParams.mStaticTimeThresholdAfterFastTyping) {
+ mAfterFastTyping = true;
+ }
+ if (DEBUG) {
+ Log.d(TAG, String.format("[%d] onDownEvent: dT=%3d%s", mPointerId,
+ elapsedTimeAfterTyping, mAfterFastTyping ? " afterFastTyping" : ""));
+ }
+ final int elapsedTimeFromFirstDown = (int)(downTime - gestureFirstDownTime);
+ addPoint(x, y, elapsedTimeFromFirstDown, true /* isMajorEvent */);
+ }
+
+ private int getGestureDynamicDistanceThreshold(final int deltaTime) {
+ if (!mAfterFastTyping || deltaTime >= mParams.mDynamicThresholdDecayDuration) {
+ return mGestureDynamicDistanceThresholdTo;
+ }
+ final int decayedThreshold =
+ (mGestureDynamicDistanceThresholdFrom - mGestureDynamicDistanceThresholdTo)
+ * deltaTime / mParams.mDynamicThresholdDecayDuration;
+ return mGestureDynamicDistanceThresholdFrom - decayedThreshold;
+ }
+
+ private int getGestureDynamicTimeThreshold(final int deltaTime) {
+ if (!mAfterFastTyping || deltaTime >= mParams.mDynamicThresholdDecayDuration) {
+ return mParams.mDynamicTimeThresholdTo;
+ }
+ final int decayedThreshold =
+ (mParams.mDynamicTimeThresholdFrom - mParams.mDynamicTimeThresholdTo)
+ * deltaTime / mParams.mDynamicThresholdDecayDuration;
+ return mParams.mDynamicTimeThresholdFrom - decayedThreshold;
+ }
+
+ public final boolean isStartOfAGesture() {
+ if (!hasDetectedFastMove()) {
+ return false;
+ }
+ final int size = mEventTimes.getLength();
+ if (size <= 0) {
+ return false;
+ }
+ final int lastIndex = size - 1;
+ final int deltaTime = mEventTimes.get(lastIndex) - mDetectFastMoveTime;
+ if (deltaTime < 0) {
+ return false;
+ }
+ final int deltaDistance = getDistance(
+ mXCoordinates.get(lastIndex), mYCoordinates.get(lastIndex),
+ mDetectFastMoveX, mDetectFastMoveY);
+ final int distanceThreshold = getGestureDynamicDistanceThreshold(deltaTime);
+ final int timeThreshold = getGestureDynamicTimeThreshold(deltaTime);
+ final boolean isStartOfAGesture = deltaTime >= timeThreshold
+ && deltaDistance >= distanceThreshold;
+ if (DEBUG) {
+ Log.d(TAG, String.format("[%d] isStartOfAGesture: dT=%3d tT=%3d dD=%3d tD=%3d%s%s",
+ mPointerId, deltaTime, timeThreshold,
+ deltaDistance, distanceThreshold,
+ mAfterFastTyping ? " afterFastTyping" : "",
+ isStartOfAGesture ? " startOfAGesture" : ""));
+ }
+ return isStartOfAGesture;
+ }
+
+ protected void reset() {
+ mIncrementalRecognitionSize = 0;
+ mLastIncrementalBatchSize = 0;
+ mEventTimes.setLength(0);
+ mXCoordinates.setLength(0);
+ mYCoordinates.setLength(0);
+ mLastMajorEventTime = 0;
+ mDetectFastMoveTime = 0;
+ mAfterFastTyping = false;
+ }
+
+ private void appendPoint(final int x, final int y, final int time) {
+ mEventTimes.add(time);
+ mXCoordinates.add(x);
+ mYCoordinates.add(y);
+ }
+
+ private void updateMajorEvent(final int x, final int y, final int time) {
+ mLastMajorEventTime = time;
+ mLastMajorEventX = x;
+ mLastMajorEventY = y;
+ }
+
+ private final boolean hasDetectedFastMove() {
+ return mDetectFastMoveTime > 0;
+ }
+
+ private int detectFastMove(final int x, final int y, final int time) {
+ final int size = mEventTimes.getLength();
+ final int lastIndex = size - 1;
+ final int lastX = mXCoordinates.get(lastIndex);
+ final int lastY = mYCoordinates.get(lastIndex);
+ final int dist = getDistance(lastX, lastY, x, y);
+ final int msecs = time - mEventTimes.get(lastIndex);
+ if (msecs > 0) {
+ final int pixels = getDistance(lastX, lastY, x, y);
+ final int pixelsPerSec = pixels * MSEC_PER_SEC;
+ if (DEBUG_SPEED) {
+ final float speed = (float)pixelsPerSec / msecs / mKeyWidth;
+ Log.d(TAG, String.format("[%d] detectFastMove: speed=%5.2f", mPointerId, speed));
+ }
+ // Equivalent to (pixels / msecs < mStartSpeedThreshold / MSEC_PER_SEC)
+ if (!hasDetectedFastMove() && pixelsPerSec > mDetectFastMoveSpeedThreshold * msecs) {
+ if (DEBUG) {
+ final float speed = (float)pixelsPerSec / msecs / mKeyWidth;
+ Log.d(TAG, String.format(
+ "[%d] detectFastMove: speed=%5.2f T=%3d points=%3d fastMove",
+ mPointerId, speed, time, size));
+ }
+ mDetectFastMoveTime = time;
+ mDetectFastMoveX = x;
+ mDetectFastMoveY = y;
+ }
+ }
+ return dist;
+ }
+
+ public void addPoint(final int x, final int y, final int time, final boolean isMajorEvent) {
+ final int size = mEventTimes.getLength();
+ if (size <= 0) {
+ // Down event
+ appendPoint(x, y, time);
+ updateMajorEvent(x, y, time);
+ } else {
+ final int distance = detectFastMove(x, y, time);
+ if (distance > mGestureSamplingMinimumDistance) {
+ appendPoint(x, y, time);
+ }
+ }
+ if (isMajorEvent) {
+ updateIncrementalRecognitionSize(x, y, time);
+ updateMajorEvent(x, y, time);
+ }
+ }
+
+ private void updateIncrementalRecognitionSize(final int x, final int y, final int time) {
+ final int msecs = (int)(time - mLastMajorEventTime);
+ if (msecs <= 0) {
+ return;
+ }
+ final int pixels = getDistance(mLastMajorEventX, mLastMajorEventY, x, y);
+ final int pixelsPerSec = pixels * MSEC_PER_SEC;
+ // Equivalent to (pixels / msecs < mGestureRecognitionThreshold / MSEC_PER_SEC)
+ if (pixelsPerSec < mGestureRecognitionSpeedThreshold * msecs) {
+ mIncrementalRecognitionSize = mEventTimes.getLength();
+ }
+ }
+
+ public final boolean hasRecognitionTimePast(
+ final long currentTime, final long lastRecognitionTime) {
+ return currentTime > lastRecognitionTime + mParams.mRecognitionMinimumTime;
+ }
+
+ public final void appendAllBatchPoints(final InputPointers out) {
+ appendBatchPoints(out, mEventTimes.getLength());
+ }
+
+ public final void appendIncrementalBatchPoints(final InputPointers out) {
+ appendBatchPoints(out, mIncrementalRecognitionSize);
+ }
+
+ private void appendBatchPoints(final InputPointers out, final int size) {
+ final int length = size - mLastIncrementalBatchSize;
+ if (length <= 0) {
+ return;
+ }
+ out.append(mPointerId, mEventTimes, mXCoordinates, mYCoordinates,
+ mLastIncrementalBatchSize, length);
+ mLastIncrementalBatchSize = size;
+ }
+
+ private static int getDistance(final int x1, final int y1, final int x2, final int y2) {
+ final int dx = x1 - x2;
+ final int dy = y1 - y2;
+ // Note that, in recent versions of Android, FloatMath is actually slower than
+ // java.lang.Math due to the way the JIT optimizes java.lang.Math.
+ return (int)Math.sqrt(dx * dx + dy * dy);
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
new file mode 100644
index 000000000..8192c9076
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
@@ -0,0 +1,94 @@
+/*
+ * 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.keyboard.internal;
+
+import com.android.inputmethod.latin.ResizableIntArray;
+
+public final class GestureStrokeWithPreviewPoints extends GestureStroke {
+ public static final int PREVIEW_CAPACITY = 256;
+
+ private final ResizableIntArray mPreviewEventTimes = new ResizableIntArray(PREVIEW_CAPACITY);
+ private final ResizableIntArray mPreviewXCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
+ private final ResizableIntArray mPreviewYCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
+
+ private int mStrokeId;
+ private int mLastPreviewSize;
+
+ private int mMinPreviewSampleLengthSquare;
+ private int mLastX;
+ private int mLastY;
+
+ // TODO: Move this to resource.
+ private static final float MIN_PREVIEW_SAMPLE_LENGTH_RATIO_TO_KEY_WIDTH = 0.1f;
+
+ public GestureStrokeWithPreviewPoints(final int pointerId, final GestureStrokeParams params) {
+ super(pointerId, params);
+ }
+
+ @Override
+ protected void reset() {
+ super.reset();
+ mStrokeId++;
+ mLastPreviewSize = 0;
+ mPreviewEventTimes.setLength(0);
+ mPreviewXCoordinates.setLength(0);
+ mPreviewYCoordinates.setLength(0);
+ }
+
+ public int getGestureStrokeId() {
+ return mStrokeId;
+ }
+
+ public int getGestureStrokePreviewSize() {
+ return mPreviewEventTimes.getLength();
+ }
+
+ @Override
+ public void setKeyboardGeometry(final int keyWidth) {
+ super.setKeyboardGeometry(keyWidth);
+ final float sampleLength = keyWidth * MIN_PREVIEW_SAMPLE_LENGTH_RATIO_TO_KEY_WIDTH;
+ mMinPreviewSampleLengthSquare = (int)(sampleLength * sampleLength);
+ }
+
+ private boolean needsSampling(final int x, final int y) {
+ final int dx = x - mLastX;
+ final int dy = y - mLastY;
+ return dx * dx + dy * dy >= mMinPreviewSampleLengthSquare;
+ }
+
+ @Override
+ public void addPoint(final int x, final int y, final int time, final boolean isMajorEvent) {
+ super.addPoint(x, y, time, isMajorEvent);
+ if (isMajorEvent || needsSampling(x, y)) {
+ mPreviewEventTimes.add(time);
+ mPreviewXCoordinates.add(x);
+ mPreviewYCoordinates.add(y);
+ mLastX = x;
+ mLastY = y;
+ }
+ }
+
+ public void appendPreviewStroke(final ResizableIntArray eventTimes,
+ final ResizableIntArray xCoords, final ResizableIntArray yCoords) {
+ final int length = mPreviewEventTimes.getLength() - mLastPreviewSize;
+ if (length <= 0) {
+ return;
+ }
+ eventTimes.append(mPreviewEventTimes, mLastPreviewSize, length);
+ xCoords.append(mPreviewXCoordinates, mLastPreviewSize, length);
+ yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length);
+ mLastPreviewSize = mPreviewEventTimes.getLength();
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java
new file mode 100644
index 000000000..5dcd842f7
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyDrawParams.java
@@ -0,0 +1,140 @@
+/*
+ * 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.keyboard.internal;
+
+import android.graphics.Typeface;
+
+import com.android.inputmethod.latin.ResourceUtils;
+
+public final class KeyDrawParams {
+ public Typeface mTypeface;
+
+ public int mLetterSize;
+ public int mLabelSize;
+ public int mLargeLetterSize;
+ public int mLargeLabelSize;
+ public int mHintLetterSize;
+ public int mShiftedLetterHintSize;
+ public int mHintLabelSize;
+ public int mPreviewTextSize;
+
+ public int mTextColor;
+ public int mTextInactivatedColor;
+ public int mTextShadowColor;
+ public int mHintLetterColor;
+ public int mHintLabelColor;
+ public int mShiftedLetterHintInactivatedColor;
+ public int mShiftedLetterHintActivatedColor;
+ public int mPreviewTextColor;
+
+ public int mAnimAlpha;
+
+ public KeyDrawParams() {}
+
+ private KeyDrawParams(final KeyDrawParams copyFrom) {
+ mTypeface = copyFrom.mTypeface;
+
+ mLetterSize = copyFrom.mLetterSize;
+ mLabelSize = copyFrom.mLabelSize;
+ mLargeLetterSize = copyFrom.mLargeLetterSize;
+ mLargeLabelSize = copyFrom.mLargeLabelSize;
+ mHintLetterSize = copyFrom.mHintLetterSize;
+ mShiftedLetterHintSize = copyFrom.mShiftedLetterHintSize;
+ mHintLabelSize = copyFrom.mHintLabelSize;
+ mPreviewTextSize = copyFrom.mPreviewTextSize;
+
+ mTextColor = copyFrom.mTextColor;
+ mTextInactivatedColor = copyFrom.mTextInactivatedColor;
+ mTextShadowColor = copyFrom.mTextShadowColor;
+ mHintLetterColor = copyFrom.mHintLetterColor;
+ mHintLabelColor = copyFrom.mHintLabelColor;
+ mShiftedLetterHintInactivatedColor = copyFrom.mShiftedLetterHintInactivatedColor;
+ mShiftedLetterHintActivatedColor = copyFrom.mShiftedLetterHintActivatedColor;
+ mPreviewTextColor = copyFrom.mPreviewTextColor;
+
+ mAnimAlpha = copyFrom.mAnimAlpha;
+ }
+
+ public void updateParams(final int keyHeight, final KeyVisualAttributes attr) {
+ if (attr == null) {
+ return;
+ }
+
+ if (attr.mTypeface != null) {
+ mTypeface = attr.mTypeface;
+ }
+
+ mLetterSize = selectTextSizeFromDimensionOrRatio(keyHeight,
+ attr.mLetterSize, attr.mLetterRatio, mLetterSize);
+ mLabelSize = selectTextSizeFromDimensionOrRatio(keyHeight,
+ attr.mLabelSize, attr.mLabelRatio, mLabelSize);
+ mLargeLabelSize = selectTextSize(keyHeight, attr.mLargeLabelRatio, mLargeLabelSize);
+ mLargeLetterSize = selectTextSize(keyHeight, attr.mLargeLetterRatio, mLargeLetterSize);
+ mHintLetterSize = selectTextSize(keyHeight, attr.mHintLetterRatio, mHintLetterSize);
+ mShiftedLetterHintSize = selectTextSize(keyHeight,
+ attr.mShiftedLetterHintRatio, mShiftedLetterHintSize);
+ mHintLabelSize = selectTextSize(keyHeight, attr.mHintLabelRatio, mHintLabelSize);
+ mPreviewTextSize = selectTextSize(keyHeight, attr.mPreviewTextRatio, mPreviewTextSize);
+
+ mTextColor = selectColor(attr.mTextColor, mTextColor);
+ mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor);
+ mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor);
+ mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor);
+ mHintLabelColor = selectColor(attr.mHintLabelColor, mHintLabelColor);
+ mShiftedLetterHintInactivatedColor = selectColor(
+ attr.mShiftedLetterHintInactivatedColor, mShiftedLetterHintInactivatedColor);
+ mShiftedLetterHintActivatedColor = selectColor(
+ attr.mShiftedLetterHintActivatedColor, mShiftedLetterHintActivatedColor);
+ mPreviewTextColor = selectColor(attr.mPreviewTextColor, mPreviewTextColor);
+ }
+
+ public KeyDrawParams mayCloneAndUpdateParams(final int keyHeight,
+ final KeyVisualAttributes attr) {
+ if (attr == null) {
+ return this;
+ }
+ final KeyDrawParams newParams = new KeyDrawParams(this);
+ newParams.updateParams(keyHeight, attr);
+ return newParams;
+ }
+
+ private static final int selectTextSizeFromDimensionOrRatio(final int keyHeight,
+ final int dimens, final float ratio, final int defaultDimens) {
+ if (ResourceUtils.isValidDimensionPixelSize(dimens)) {
+ return dimens;
+ }
+ if (ResourceUtils.isValidFraction(ratio)) {
+ return (int)(keyHeight * ratio);
+ }
+ return defaultDimens;
+ }
+
+ private static final int selectTextSize(final int keyHeight, final float ratio,
+ final int defaultSize) {
+ if (ResourceUtils.isValidFraction(ratio)) {
+ return (int)(keyHeight * ratio);
+ }
+ return defaultSize;
+ }
+
+ private static final int selectColor(final int attrColor, final int defaultColor) {
+ if (attrColor != 0) {
+ return attrColor;
+ }
+ return defaultColor;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java
new file mode 100644
index 000000000..609d1a57f
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewDrawParams.java
@@ -0,0 +1,44 @@
+/*
+ * 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.keyboard.internal;
+
+public final class KeyPreviewDrawParams {
+ // The graphical geometry of the key preview.
+ // <-width->
+ // +-------+ ^
+ // | | |
+ // |preview| height (visible)
+ // | | |
+ // + + ^ v
+ // \ / |offset
+ // +-\ /-+ v
+ // | +-+ |
+ // |parent |
+ // | key|
+ // +-------+
+ // The background of a {@link TextView} being used for a key preview may have invisible
+ // paddings. To align the more keys keyboard panel's visible part with the visible part of
+ // the background, we need to record the width and height of key preview that don't include
+ // invisible paddings.
+ public int mPreviewVisibleWidth;
+ public int mPreviewVisibleHeight;
+ // The key preview may have an arbitrary offset and its background that may have a bottom
+ // padding. To align the more keys keyboard and the key preview we also need to record the
+ // offset between the top edge of parent key and the bottom of the visible part of key
+ // preview background.
+ public int mPreviewVisibleOffset;
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
index c4452a5f5..2caa5eb02 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -21,6 +21,7 @@ import static com.android.inputmethod.keyboard.Keyboard.CODE_UNSPECIFIED;
import android.text.TextUtils;
import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.StringUtils;
@@ -45,7 +46,7 @@ import java.util.Locale;
* Note that the '\' is also parsed by XML parser and CSV parser as well.
* See {@link KeyboardIconsSet} about icon_name.
*/
-public class KeySpecParser {
+public final class KeySpecParser {
private static final boolean DEBUG = LatinImeLogger.sDBG;
private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
@@ -55,38 +56,20 @@ public class KeySpecParser {
private static final char ESCAPE_CHAR = '\\';
private static final char LABEL_END = '|';
private static final String PREFIX_TEXT = "!text/";
- private static final String PREFIX_ICON = "!icon/";
+ static final String PREFIX_ICON = "!icon/";
private static final String PREFIX_CODE = "!code/";
private static final String PREFIX_HEX = "0x";
private static final String ADDITIONAL_MORE_KEY_MARKER = "%";
- public static class MoreKeySpec {
- public final int mCode;
- public final String mLabel;
- public final String mOutputText;
- public final int mIconId;
-
- public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, Locale locale,
- final KeyboardCodesSet codesSet) {
- mCode = toUpperCaseOfCodeForLocale(getCode(moreKeySpec, codesSet),
- needsToUpperCase, locale);
- mLabel = toUpperCaseOfStringForLocale(getLabel(moreKeySpec),
- needsToUpperCase, locale);
- mOutputText = toUpperCaseOfStringForLocale(getOutputText(moreKeySpec),
- needsToUpperCase, locale);
- mIconId = getIconId(moreKeySpec);
- }
- }
-
private KeySpecParser() {
// Intentional empty constructor for utility class.
}
- private static boolean hasIcon(String moreKeySpec) {
+ private static boolean hasIcon(final String moreKeySpec) {
return moreKeySpec.startsWith(PREFIX_ICON);
}
- private static boolean hasCode(String moreKeySpec) {
+ private static boolean hasCode(final String moreKeySpec) {
final int end = indexOfLabelEnd(moreKeySpec, 0);
if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith(
PREFIX_CODE, end + 1)) {
@@ -95,7 +78,7 @@ public class KeySpecParser {
return false;
}
- private static String parseEscape(String text) {
+ private static String parseEscape(final String text) {
if (text.indexOf(ESCAPE_CHAR) < 0) {
return text;
}
@@ -114,7 +97,7 @@ public class KeySpecParser {
return sb.toString();
}
- private static int indexOfLabelEnd(String moreKeySpec, int start) {
+ private static int indexOfLabelEnd(final String moreKeySpec, final int start) {
if (moreKeySpec.indexOf(ESCAPE_CHAR, start) < 0) {
final int end = moreKeySpec.indexOf(LABEL_END, start);
if (end == 0) {
@@ -135,7 +118,7 @@ public class KeySpecParser {
return -1;
}
- public static String getLabel(String moreKeySpec) {
+ public static String getLabel(final String moreKeySpec) {
if (hasIcon(moreKeySpec)) {
return null;
}
@@ -148,7 +131,7 @@ public class KeySpecParser {
return label;
}
- private static String getOutputTextInternal(String moreKeySpec) {
+ private static String getOutputTextInternal(final String moreKeySpec) {
final int end = indexOfLabelEnd(moreKeySpec, 0);
if (end <= 0) {
return null;
@@ -159,7 +142,7 @@ public class KeySpecParser {
return parseEscape(moreKeySpec.substring(end + /* LABEL_END */1));
}
- static String getOutputText(String moreKeySpec) {
+ static String getOutputText(final String moreKeySpec) {
if (hasCode(moreKeySpec)) {
return null;
}
@@ -183,7 +166,7 @@ public class KeySpecParser {
return (StringUtils.codePointCount(label) == 1) ? null : label;
}
- static int getCode(String moreKeySpec, KeyboardCodesSet codesSet) {
+ static int getCode(final String moreKeySpec, final KeyboardCodesSet codesSet) {
if (hasCode(moreKeySpec)) {
final int end = indexOfLabelEnd(moreKeySpec, 0);
if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
@@ -208,7 +191,8 @@ public class KeySpecParser {
return Keyboard.CODE_OUTPUT_TEXT;
}
- public static int parseCode(String text, KeyboardCodesSet codesSet, int defCode) {
+ public static int parseCode(final String text, final KeyboardCodesSet codesSet,
+ final int defCode) {
if (text == null) return defCode;
if (text.startsWith(PREFIX_CODE)) {
return codesSet.getCode(text.substring(PREFIX_CODE.length()));
@@ -219,7 +203,7 @@ public class KeySpecParser {
}
}
- public static int getIconId(String moreKeySpec) {
+ public static int getIconId(final String moreKeySpec) {
if (moreKeySpec != null && hasIcon(moreKeySpec)) {
final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length());
final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length())
@@ -229,7 +213,7 @@ public class KeySpecParser {
return KeyboardIconsSet.ICON_UNDEFINED;
}
- private static <T> ArrayList<T> arrayAsList(T[] array, int start, int end) {
+ private static <T> ArrayList<T> arrayAsList(final T[] array, final int start, final int end) {
if (array == null) {
throw new NullPointerException();
}
@@ -237,7 +221,7 @@ public class KeySpecParser {
throw new IllegalArgumentException();
}
- final ArrayList<T> list = new ArrayList<T>(end - start);
+ final ArrayList<T> list = CollectionUtils.newArrayList(end - start);
for (int i = start; i < end; i++) {
list.add(array[i]);
}
@@ -246,7 +230,7 @@ public class KeySpecParser {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
- private static String[] filterOutEmptyString(String[] array) {
+ private static String[] filterOutEmptyString(final String[] array) {
if (array == null) {
return EMPTY_STRING_ARRAY;
}
@@ -267,8 +251,8 @@ public class KeySpecParser {
return out.toArray(new String[out.size()]);
}
- public static String[] insertAdditionalMoreKeys(String[] moreKeySpecs,
- String[] additionalMoreKeySpecs) {
+ public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
+ final String[] additionalMoreKeySpecs) {
final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
final int moreKeysCount = moreKeys.length;
@@ -334,13 +318,14 @@ public class KeySpecParser {
}
@SuppressWarnings("serial")
- public static class KeySpecParserError extends RuntimeException {
- public KeySpecParserError(String message) {
+ public static final class KeySpecParserError extends RuntimeException {
+ public KeySpecParserError(final String message) {
super(message);
}
}
- public static String resolveTextReference(String rawText, KeyboardTextsSet textsSet) {
+ public static String resolveTextReference(final String rawText,
+ final KeyboardTextsSet textsSet) {
int level = 0;
String text = rawText;
StringBuilder sb;
@@ -386,7 +371,7 @@ public class KeySpecParser {
return text;
}
- private static int searchTextNameEnd(String text, int start) {
+ private static int searchTextNameEnd(final String text, final int start) {
final int size = text.length();
for (int pos = start; pos < size; pos++) {
final char c = text.charAt(pos);
@@ -399,7 +384,7 @@ public class KeySpecParser {
return size;
}
- public static String[] parseCsvString(String rawText, KeyboardTextsSet textsSet) {
+ public static String[] parseCsvString(final String rawText, final KeyboardTextsSet textsSet) {
final String text = resolveTextReference(rawText, textsSet);
final int size = text.length();
if (size == 0) {
@@ -417,7 +402,7 @@ public class KeySpecParser {
// Skip empty entry.
if (pos - start > 0) {
if (list == null) {
- list = new ArrayList<String>();
+ list = CollectionUtils.newArrayList();
}
list.add(text.substring(start, pos));
}
@@ -438,7 +423,8 @@ public class KeySpecParser {
return list.toArray(new String[list.size()]);
}
- public static int getIntValue(String[] moreKeys, String key, int defaultValue) {
+ public static int getIntValue(final String[] moreKeys, final String key,
+ final int defaultValue) {
if (moreKeys == null) {
return defaultValue;
}
@@ -464,7 +450,7 @@ public class KeySpecParser {
return value;
}
- public static boolean getBooleanValue(String[] moreKeys, String key) {
+ public static boolean getBooleanValue(final String[] moreKeys, final String key) {
if (moreKeys == null) {
return false;
}
@@ -480,8 +466,8 @@ public class KeySpecParser {
return value;
}
- public static int toUpperCaseOfCodeForLocale(int code, boolean needsToUpperCase,
- Locale locale) {
+ public static int toUpperCaseOfCodeForLocale(final int code, final boolean needsToUpperCase,
+ final Locale locale) {
if (!Keyboard.isLetterCode(code) || !needsToUpperCase) return code;
final String text = new String(new int[] { code } , 0, 1);
final String casedText = KeySpecParser.toUpperCaseOfStringForLocale(
@@ -490,8 +476,8 @@ public class KeySpecParser {
? casedText.codePointAt(0) : CODE_UNSPECIFIED;
}
- public static String toUpperCaseOfStringForLocale(String text, boolean needsToUpperCase,
- Locale locale) {
+ public static String toUpperCaseOfStringForLocale(final String text,
+ final boolean needsToUpperCase, final Locale locale) {
if (text == null || !needsToUpperCase) return text;
return text.toUpperCase(locale);
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
new file mode 100644
index 000000000..e8cacf9e7
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -0,0 +1,46 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.res.TypedArray;
+
+public abstract class KeyStyle {
+ private final KeyboardTextsSet mTextsSet;
+
+ 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);
+
+ protected KeyStyle(final KeyboardTextsSet textsSet) {
+ mTextsSet = textsSet;
+ }
+
+ protected String parseString(final TypedArray a, final int index) {
+ if (a.hasValue(index)) {
+ return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet);
+ }
+ return null;
+ }
+
+ protected String[] parseStringArray(final TypedArray a, final int index) {
+ if (a.hasValue(index)) {
+ return KeySpecParser.parseCsvString(a.getString(index), mTextsSet);
+ }
+ return null;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
index 80f4f259b..563d22414 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStylesSet.java
@@ -18,8 +18,9 @@ package com.android.inputmethod.keyboard.internal;
import android.content.res.TypedArray;
import android.util.Log;
+import android.util.SparseArray;
-import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.XmlParseUtils;
@@ -28,120 +29,111 @@ import org.xmlpull.v1.XmlPullParserException;
import java.util.HashMap;
-public class KeyStyles {
- private static final String TAG = KeyStyles.class.getSimpleName();
+public final class KeyStylesSet {
+ private static final String TAG = KeyStylesSet.class.getSimpleName();
private static final boolean DEBUG = false;
- final HashMap<String, KeyStyle> mStyles = new HashMap<String, KeyStyle>();
+ private final HashMap<String, KeyStyle> mStyles = CollectionUtils.newHashMap();
- final KeyboardTextsSet mTextsSet;
+ private final KeyboardTextsSet mTextsSet;
private final KeyStyle mEmptyKeyStyle;
private static final String EMPTY_STYLE_NAME = "<empty>";
- public KeyStyles(KeyboardTextsSet textsSet) {
+ public KeyStylesSet(final KeyboardTextsSet textsSet) {
mTextsSet = textsSet;
- mEmptyKeyStyle = new EmptyKeyStyle();
+ mEmptyKeyStyle = new EmptyKeyStyle(textsSet);
mStyles.put(EMPTY_STYLE_NAME, mEmptyKeyStyle);
}
- 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);
-
- protected String parseString(TypedArray a, int index) {
- if (a.hasValue(index)) {
- return KeySpecParser.resolveTextReference(a.getString(index), mTextsSet);
- }
- return null;
- }
-
- protected String[] parseStringArray(TypedArray a, int index) {
- if (a.hasValue(index)) {
- return KeySpecParser.parseCsvString(a.getString(index), mTextsSet);
- }
- return null;
+ private static final class EmptyKeyStyle extends KeyStyle {
+ EmptyKeyStyle(final KeyboardTextsSet textsSet) {
+ super(textsSet);
}
- }
- class EmptyKeyStyle extends KeyStyle {
@Override
- public String[] getStringArray(TypedArray a, int index) {
+ public String[] getStringArray(final TypedArray a, final int index) {
return parseStringArray(a, index);
}
@Override
- public String getString(TypedArray a, int index) {
+ public String getString(final TypedArray a, final int index) {
return parseString(a, index);
}
@Override
- public int getInt(TypedArray a, int index, int defaultValue) {
+ public int getInt(final TypedArray a, final int index, final int defaultValue) {
return a.getInt(index, defaultValue);
}
@Override
- public int getFlag(TypedArray a, int index) {
+ public int getFlag(final TypedArray a, final int index) {
return a.getInt(index, 0);
}
}
- private class DeclaredKeyStyle extends KeyStyle {
+ private static final class DeclaredKeyStyle extends KeyStyle {
+ private final HashMap<String, KeyStyle> mStyles;
private final String mParentStyleName;
- private final HashMap<Integer, Object> mStyleAttributes = new HashMap<Integer, Object>();
+ private final SparseArray<Object> mStyleAttributes = CollectionUtils.newSparseArray();
- public DeclaredKeyStyle(String parentStyleName) {
+ public DeclaredKeyStyle(final String parentStyleName, final KeyboardTextsSet textsSet,
+ final HashMap<String, KeyStyle> styles) {
+ super(textsSet);
mParentStyleName = parentStyleName;
+ mStyles = styles;
}
@Override
- public String[] getStringArray(TypedArray a, int index) {
+ public String[] getStringArray(final TypedArray a, final int index) {
if (a.hasValue(index)) {
return parseStringArray(a, index);
}
- if (mStyleAttributes.containsKey(index)) {
- return (String[])mStyleAttributes.get(index);
+ final Object value = mStyleAttributes.get(index);
+ if (value != null) {
+ return (String[])value;
}
final KeyStyle parentStyle = mStyles.get(mParentStyleName);
return parentStyle.getStringArray(a, index);
}
@Override
- public String getString(TypedArray a, int index) {
+ public String getString(final TypedArray a, final int index) {
if (a.hasValue(index)) {
return parseString(a, index);
}
- if (mStyleAttributes.containsKey(index)) {
- return (String)mStyleAttributes.get(index);
+ final Object value = mStyleAttributes.get(index);
+ if (value != null) {
+ return (String)value;
}
final KeyStyle parentStyle = mStyles.get(mParentStyleName);
return parentStyle.getString(a, index);
}
@Override
- public int getInt(TypedArray a, int index, int defaultValue) {
+ public int getInt(final TypedArray a, final int index, final int defaultValue) {
if (a.hasValue(index)) {
return a.getInt(index, defaultValue);
}
- if (mStyleAttributes.containsKey(index)) {
- return (Integer)mStyleAttributes.get(index);
+ final Object value = mStyleAttributes.get(index);
+ if (value != null) {
+ return (Integer)value;
}
final KeyStyle parentStyle = mStyles.get(mParentStyleName);
return parentStyle.getInt(a, index, defaultValue);
}
@Override
- public int getFlag(TypedArray a, int index) {
- int value = a.getInt(index, 0);
- if (mStyleAttributes.containsKey(index)) {
- value |= (Integer)mStyleAttributes.get(index);
+ 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 value | parentStyle.getFlag(a, index);
+ return flags | parentStyle.getFlag(a, index);
}
- void readKeyAttributes(TypedArray keyAttr) {
+ public void readKeyAttributes(final TypedArray keyAttr) {
// TODO: Currently not all Key attributes can be declared as style.
readString(keyAttr, R.styleable.Keyboard_Key_code);
readString(keyAttr, R.styleable.Keyboard_Key_altCode);
@@ -159,38 +151,38 @@ public class KeyStyles {
readFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags);
}
- private void readString(TypedArray a, int index) {
+ private void readString(final TypedArray a, final int index) {
if (a.hasValue(index)) {
mStyleAttributes.put(index, parseString(a, index));
}
}
- private void readInt(TypedArray a, int index) {
+ private void readInt(final TypedArray a, final int index) {
if (a.hasValue(index)) {
mStyleAttributes.put(index, a.getInt(index, 0));
}
}
- private void readFlag(TypedArray a, int index) {
+ private void readFlag(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));
}
}
- private void readStringArray(TypedArray a, int index) {
+ private void readStringArray(final TypedArray a, final int index) {
if (a.hasValue(index)) {
mStyleAttributes.put(index, parseStringArray(a, index));
}
}
}
- public void parseKeyStyleAttributes(TypedArray keyStyleAttr, TypedArray keyAttrs,
- XmlPullParser parser) throws XmlPullParserException {
+ public void parseKeyStyleAttributes(final TypedArray keyStyleAttr, final TypedArray keyAttrs,
+ final XmlPullParser parser) throws XmlPullParserException {
final String styleName = keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName);
if (DEBUG) {
Log.d(TAG, String.format("<%s styleName=%s />",
- Keyboard.Builder.TAG_KEY_STYLE, styleName));
+ KeyboardBuilder.TAG_KEY_STYLE, styleName));
if (mStyles.containsKey(styleName)) {
Log.d(TAG, "key-style " + styleName + " is overridden at "
+ parser.getPositionDescription());
@@ -205,12 +197,12 @@ public class KeyStyles {
"Unknown parentStyle " + parentStyleName, parser);
}
}
- final DeclaredKeyStyle style = new DeclaredKeyStyle(parentStyleName);
+ final DeclaredKeyStyle style = new DeclaredKeyStyle(parentStyleName, mTextsSet, mStyles);
style.readKeyAttributes(keyAttrs);
mStyles.put(styleName, style);
}
- public KeyStyle getKeyStyle(TypedArray keyAttr, XmlPullParser parser)
+ public KeyStyle getKeyStyle(final TypedArray keyAttr, final XmlPullParser parser)
throws XmlParseUtils.ParseException {
if (!keyAttr.hasValue(R.styleable.Keyboard_Key_keyStyle)) {
return mEmptyKeyStyle;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java
new file mode 100644
index 000000000..6ddd2a6fb
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyVisualAttributes.java
@@ -0,0 +1,130 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+import android.util.SparseIntArray;
+
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResourceUtils;
+
+public final class KeyVisualAttributes {
+ public final Typeface mTypeface;
+
+ public final float mLetterRatio;
+ public final int mLetterSize;
+ public final float mLabelRatio;
+ public final int mLabelSize;
+ public final float mLargeLetterRatio;
+ public final float mLargeLabelRatio;
+ public final float mHintLetterRatio;
+ public final float mShiftedLetterHintRatio;
+ public final float mHintLabelRatio;
+ public final float mPreviewTextRatio;
+
+ public final int mTextColor;
+ public final int mTextInactivatedColor;
+ public final int mTextShadowColor;
+ public final int mHintLetterColor;
+ public final int mHintLabelColor;
+ public final int mShiftedLetterHintInactivatedColor;
+ public final int mShiftedLetterHintActivatedColor;
+ public final int mPreviewTextColor;
+
+ private static final int[] VISUAL_ATTRIBUTE_IDS = {
+ R.styleable.Keyboard_Key_keyTypeface,
+ R.styleable.Keyboard_Key_keyLetterSize,
+ R.styleable.Keyboard_Key_keyLabelSize,
+ R.styleable.Keyboard_Key_keyLargeLetterRatio,
+ R.styleable.Keyboard_Key_keyLargeLabelRatio,
+ R.styleable.Keyboard_Key_keyHintLetterRatio,
+ R.styleable.Keyboard_Key_keyShiftedLetterHintRatio,
+ R.styleable.Keyboard_Key_keyHintLabelRatio,
+ R.styleable.Keyboard_Key_keyPreviewTextRatio,
+ R.styleable.Keyboard_Key_keyTextColor,
+ R.styleable.Keyboard_Key_keyTextInactivatedColor,
+ R.styleable.Keyboard_Key_keyTextShadowColor,
+ R.styleable.Keyboard_Key_keyHintLetterColor,
+ R.styleable.Keyboard_Key_keyHintLabelColor,
+ R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor,
+ R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor,
+ R.styleable.Keyboard_Key_keyPreviewTextColor,
+ };
+ private static final SparseIntArray sVisualAttributeIds = new SparseIntArray();
+ private static final int ATTR_DEFINED = 1;
+ private static final int ATTR_NOT_FOUND = 0;
+ static {
+ for (final int attrId : VISUAL_ATTRIBUTE_IDS) {
+ sVisualAttributeIds.put(attrId, ATTR_DEFINED);
+ }
+ }
+
+ public static KeyVisualAttributes newInstance(final TypedArray keyAttr) {
+ final int indexCount = keyAttr.getIndexCount();
+ for (int i = 0; i < indexCount; i++) {
+ final int attrId = keyAttr.getIndex(i);
+ if (sVisualAttributeIds.get(attrId, ATTR_NOT_FOUND) == ATTR_NOT_FOUND) {
+ continue;
+ }
+ return new KeyVisualAttributes(keyAttr);
+ }
+ return null;
+ }
+
+ private KeyVisualAttributes(final TypedArray keyAttr) {
+ if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyTypeface)) {
+ mTypeface = Typeface.defaultFromStyle(
+ keyAttr.getInt(R.styleable.Keyboard_Key_keyTypeface, Typeface.NORMAL));
+ } else {
+ mTypeface = null;
+ }
+
+ mLetterRatio = ResourceUtils.getFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyLetterSize);
+ mLetterSize = ResourceUtils.getDimensionPixelSize(keyAttr,
+ R.styleable.Keyboard_Key_keyLetterSize);
+ mLabelRatio = ResourceUtils.getFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyLabelSize);
+ mLabelSize = ResourceUtils.getDimensionPixelSize(keyAttr,
+ R.styleable.Keyboard_Key_keyLabelSize);
+ mLargeLetterRatio = ResourceUtils.getFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyLargeLetterRatio);
+ mLargeLabelRatio = ResourceUtils.getFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyLargeLabelRatio);
+ mHintLetterRatio = ResourceUtils.getFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyHintLetterRatio);
+ mShiftedLetterHintRatio = ResourceUtils.getFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyShiftedLetterHintRatio);
+ mHintLabelRatio = ResourceUtils.getFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyHintLabelRatio);
+ mPreviewTextRatio = ResourceUtils.getFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyPreviewTextRatio);
+
+ mTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextColor, 0);
+ mTextInactivatedColor = keyAttr.getColor(
+ R.styleable.Keyboard_Key_keyTextInactivatedColor, 0);
+ mTextShadowColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyTextShadowColor, 0);
+ mHintLetterColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLetterColor, 0);
+ mHintLabelColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyHintLabelColor, 0);
+ mShiftedLetterHintInactivatedColor = keyAttr.getColor(
+ R.styleable.Keyboard_Key_keyShiftedLetterHintInactivatedColor, 0);
+ mShiftedLetterHintActivatedColor = keyAttr.getColor(
+ R.styleable.Keyboard_Key_keyShiftedLetterHintActivatedColor, 0);
+ mPreviewTextColor = keyAttr.getColor(R.styleable.Keyboard_Key_keyPreviewTextColor, 0);
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
new file mode 100644
index 000000000..b314a3795
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -0,0 +1,813 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+import android.view.InflateException;
+
+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.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 org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Keyboard Building helper.
+ *
+ * This class parses Keyboard XML file and eventually build a Keyboard.
+ * The Keyboard XML file looks like:
+ * <pre>
+ * &lt;!-- xml/keyboard.xml --&gt;
+ * &lt;Keyboard keyboard_attributes*&gt;
+ * &lt;!-- Keyboard Content --&gt;
+ * &lt;Row row_attributes*&gt;
+ * &lt;!-- Row Content --&gt;
+ * &lt;Key key_attributes* /&gt;
+ * &lt;Spacer horizontalGap="32.0dp" /&gt;
+ * &lt;include keyboardLayout="@xml/other_keys"&gt;
+ * ...
+ * &lt;/Row&gt;
+ * &lt;include keyboardLayout="@xml/other_rows"&gt;
+ * ...
+ * &lt;/Keyboard&gt;
+ * </pre>
+ * The XML file which is included in other file must have &lt;merge&gt; as root element,
+ * such as:
+ * <pre>
+ * &lt;!-- xml/other_keys.xml --&gt;
+ * &lt;merge&gt;
+ * &lt;Key key_attributes* /&gt;
+ * ...
+ * &lt;/merge&gt;
+ * </pre>
+ * and
+ * <pre>
+ * &lt;!-- xml/other_rows.xml --&gt;
+ * &lt;merge&gt;
+ * &lt;Row row_attributes*&gt;
+ * &lt;Key key_attributes* /&gt;
+ * &lt;/Row&gt;
+ * ...
+ * &lt;/merge&gt;
+ * </pre>
+ * You can also use switch-case-default tags to select Rows and Keys.
+ * <pre>
+ * &lt;switch&gt;
+ * &lt;case case_attribute*&gt;
+ * &lt;!-- Any valid tags at switch position --&gt;
+ * &lt;/case&gt;
+ * ...
+ * &lt;default&gt;
+ * &lt;!-- Any valid tags at switch position --&gt;
+ * &lt;/default&gt;
+ * &lt;/switch&gt;
+ * </pre>
+ * You can declare Key style and specify styles within Key tags.
+ * <pre>
+ * &lt;switch&gt;
+ * &lt;case mode="email"&gt;
+ * &lt;key-style styleName="f1-key" parentStyle="modifier-key"
+ * keyLabel=".com"
+ * /&gt;
+ * &lt;/case&gt;
+ * &lt;case mode="url"&gt;
+ * &lt;key-style styleName="f1-key" parentStyle="modifier-key"
+ * keyLabel="http://"
+ * /&gt;
+ * &lt;/case&gt;
+ * &lt;/switch&gt;
+ * ...
+ * &lt;Key keyStyle="shift-key" ... /&gt;
+ * </pre>
+ */
+
+public class KeyboardBuilder<KP extends KeyboardParams> {
+ private static final String BUILDER_TAG = "Keyboard.Builder";
+ private static final boolean DEBUG = false;
+
+ // Keyboard XML Tags
+ private static final String TAG_KEYBOARD = "Keyboard";
+ private static final String TAG_ROW = "Row";
+ private static final String TAG_KEY = "Key";
+ private static final String TAG_SPACER = "Spacer";
+ private static final String TAG_INCLUDE = "include";
+ private static final String TAG_MERGE = "merge";
+ private static final String TAG_SWITCH = "switch";
+ private static final String TAG_CASE = "case";
+ private static final String TAG_DEFAULT = "default";
+ public static final String TAG_KEY_STYLE = "key-style";
+
+ private static final int DEFAULT_KEYBOARD_COLUMNS = 10;
+ private static final int DEFAULT_KEYBOARD_ROWS = 4;
+
+ protected final KP mParams;
+ protected final Context mContext;
+ protected final Resources mResources;
+ private final DisplayMetrics mDisplayMetrics;
+
+ private int mCurrentY = 0;
+ private KeyboardRow mCurrentRow = null;
+ private boolean mLeftEdge;
+ private boolean mTopEdge;
+ private Key mRightEdgeKey = null;
+
+ public KeyboardBuilder(final Context context, final KP params) {
+ mContext = context;
+ final Resources res = context.getResources();
+ mResources = res;
+ mDisplayMetrics = res.getDisplayMetrics();
+
+ mParams = params;
+
+ params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
+ params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
+ }
+
+ public void setAutoGenerate(final KeysCache keysCache) {
+ mParams.mKeysCache = keysCache;
+ }
+
+ public KeyboardBuilder<KP> load(final int xmlId, final KeyboardId id) {
+ mParams.mId = id;
+ final XmlResourceParser parser = mResources.getXml(xmlId);
+ try {
+ parseKeyboard(parser);
+ } catch (XmlPullParserException e) {
+ Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
+ throw new IllegalArgumentException(e);
+ } catch (IOException e) {
+ Log.w(BUILDER_TAG, "keyboard XML parse error: " + e);
+ throw new RuntimeException(e);
+ } finally {
+ parser.close();
+ }
+ return this;
+ }
+
+ // For test only
+ public void disableTouchPositionCorrectionDataForTest() {
+ mParams.mTouchPositionCorrection.setEnabled(false);
+ }
+
+ public void setProximityCharsCorrectionEnabled(final boolean enabled) {
+ mParams.mProximityCharsCorrectionEnabled = enabled;
+ }
+
+ public Keyboard build() {
+ return new Keyboard(mParams);
+ }
+
+ private int mIndent;
+ private static final String SPACES = " ";
+
+ private static String spaces(final int count) {
+ return (count < SPACES.length()) ? SPACES.substring(0, count) : SPACES;
+ }
+
+ private void startTag(final String format, final Object ... args) {
+ Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
+ }
+
+ private void endTag(final String format, final Object ... args) {
+ Log.d(BUILDER_TAG, String.format(spaces(mIndent-- * 2) + format, args));
+ }
+
+ private void startEndTag(final String format, final Object ... args) {
+ Log.d(BUILDER_TAG, String.format(spaces(++mIndent * 2) + format, args));
+ mIndent--;
+ }
+
+ private void parseKeyboard(final XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ if (DEBUG) startTag("<%s> %s", TAG_KEYBOARD, mParams.mId);
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_KEYBOARD.equals(tag)) {
+ parseKeyboardAttributes(parser);
+ startKeyboard();
+ parseKeyboardContent(parser, false);
+ break;
+ } else {
+ throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD);
+ }
+ }
+ }
+ }
+
+ private void parseKeyboardAttributes(final XmlPullParser parser) {
+ final int displayWidth = mDisplayMetrics.widthPixels;
+ 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);
+ try {
+ final int displayHeight = mDisplayMetrics.heightPixels;
+ final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue(
+ mResources, R.array.keyboard_heights, null);
+ final float keyboardHeight;
+ if (keyboardHeightString != null) {
+ keyboardHeight = Float.parseFloat(keyboardHeightString)
+ * mDisplayMetrics.density;
+ } else {
+ keyboardHeight = keyboardAttr.getDimension(
+ R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
+ }
+ final float maxKeyboardHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
+ float minKeyboardHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
+ if (minKeyboardHeight < 0) {
+ // Specified fraction was negative, so it should be calculated against display
+ // width.
+ minKeyboardHeight = -ResourceUtils.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
+ }
+ final KeyboardParams params = mParams;
+ // Keyboard height will not exceed maxKeyboardHeight and will not be less than
+ // minKeyboardHeight.
+ params.mOccupiedHeight = (int)Math.max(
+ Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
+ params.mOccupiedWidth = params.mId.mWidth;
+ params.mTopPadding = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
+ params.mBottomPadding = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
+ params.mHorizontalEdgesPadding = (int)ResourceUtils.getDimensionOrFraction(
+ keyboardAttr,
+ R.styleable.Keyboard_keyboardHorizontalEdgesPadding,
+ mParams.mOccupiedWidth, 0);
+
+ params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
+ - params.mHorizontalCenterPadding;
+ params.mDefaultKeyWidth = (int)ResourceUtils.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
+ params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
+ params.mHorizontalGap = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
+ params.mVerticalGap = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
+ params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
+ - params.mBottomPadding + params.mVerticalGap;
+ params.mDefaultRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_rowHeight, params.mBaseHeight,
+ params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
+
+ params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
+
+ params.mMoreKeysTemplate = keyboardAttr.getResourceId(
+ R.styleable.Keyboard_moreKeysTemplate, 0);
+ params.mMaxMoreKeysKeyboardColumn = keyAttr.getInt(
+ R.styleable.Keyboard_Key_maxMoreKeysColumn, 5);
+
+ params.mThemeId = keyboardAttr.getInt(R.styleable.Keyboard_themeId, 0);
+ params.mIconsSet.loadIcons(keyboardAttr);
+ final String language = params.mId.mLocale.getLanguage();
+ params.mCodesSet.setLanguage(language);
+ params.mTextsSet.setLanguage(language);
+ final RunInLocale<Void> job = new RunInLocale<Void>() {
+ @Override
+ protected Void job(Resources res) {
+ params.mTextsSet.loadStringResources(mContext);
+ return null;
+ }
+ };
+ // Null means the current system locale.
+ final Locale locale = SubtypeLocale.isNoLanguage(params.mId.mSubtype)
+ ? null : params.mId.mLocale;
+ job.runInLocale(mResources, locale);
+
+ final int resourceId = keyboardAttr.getResourceId(
+ R.styleable.Keyboard_touchPositionCorrectionData, 0);
+ if (resourceId != 0) {
+ final String[] data = mResources.getStringArray(resourceId);
+ params.mTouchPositionCorrection.load(data);
+ }
+ } finally {
+ keyAttr.recycle();
+ keyboardAttr.recycle();
+ }
+ }
+
+ private void parseKeyboardContent(final XmlPullParser parser, final boolean skip)
+ throws XmlPullParserException, IOException {
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_ROW.equals(tag)) {
+ final KeyboardRow row = parseRowAttributes(parser);
+ if (DEBUG) startTag("<%s>%s", TAG_ROW, skip ? " skipped" : "");
+ if (!skip) {
+ startRow(row);
+ }
+ parseRowContent(parser, row, skip);
+ } else if (TAG_INCLUDE.equals(tag)) {
+ parseIncludeKeyboardContent(parser, skip);
+ } else if (TAG_SWITCH.equals(tag)) {
+ parseSwitchKeyboardContent(parser, skip);
+ } else if (TAG_KEY_STYLE.equals(tag)) {
+ parseKeyStyle(parser, skip);
+ } else {
+ throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW);
+ }
+ } else if (event == XmlPullParser.END_TAG) {
+ final String tag = parser.getName();
+ 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_ROW);
+ }
+ }
+ }
+ }
+
+ private KeyboardRow parseRowAttributes(final XmlPullParser parser)
+ throws XmlPullParserException {
+ final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard);
+ try {
+ if (a.hasValue(R.styleable.Keyboard_horizontalGap)) {
+ throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap");
+ }
+ if (a.hasValue(R.styleable.Keyboard_verticalGap)) {
+ throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap");
+ }
+ return new KeyboardRow(mResources, mParams, parser, mCurrentY);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ private void parseRowContent(final XmlPullParser parser, final KeyboardRow row,
+ final boolean skip) throws XmlPullParserException, IOException {
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_KEY.equals(tag)) {
+ parseKey(parser, row, skip);
+ } else if (TAG_SPACER.equals(tag)) {
+ parseSpacer(parser, row, skip);
+ } else if (TAG_INCLUDE.equals(tag)) {
+ parseIncludeRowContent(parser, row, skip);
+ } else if (TAG_SWITCH.equals(tag)) {
+ parseSwitchRowContent(parser, row, skip);
+ } else if (TAG_KEY_STYLE.equals(tag)) {
+ parseKeyStyle(parser, skip);
+ } else {
+ throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
+ }
+ } else if (event == XmlPullParser.END_TAG) {
+ final String tag = parser.getName();
+ if (DEBUG) endTag("</%s>", tag);
+ if (TAG_ROW.equals(tag)) {
+ 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_KEY);
+ }
+ }
+ }
+ }
+
+ private void parseKey(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+ throws XmlPullParserException, IOException {
+ if (skip) {
+ XmlParseUtils.checkEndTag(TAG_KEY, parser);
+ if (DEBUG) {
+ startEndTag("<%s /> skipped", TAG_KEY);
+ }
+ } 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));
+ }
+ XmlParseUtils.checkEndTag(TAG_KEY, parser);
+ endKey(key);
+ }
+ }
+
+ private void parseSpacer(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+ throws XmlPullParserException, IOException {
+ 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);
+ }
+ }
+
+ private void parseIncludeKeyboardContent(final XmlPullParser parser, final boolean skip)
+ throws XmlPullParserException, IOException {
+ parseIncludeInternal(parser, null, skip);
+ }
+
+ private void parseIncludeRowContent(final XmlPullParser parser, final KeyboardRow row,
+ final boolean skip) throws XmlPullParserException, IOException {
+ parseIncludeInternal(parser, row, skip);
+ }
+
+ private void parseIncludeInternal(final XmlPullParser parser, final KeyboardRow row,
+ final boolean skip) throws XmlPullParserException, IOException {
+ 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();
+ }
+
+ 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();
+ }
+ }
+ }
+
+ private void parseMerge(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+ throws XmlPullParserException, IOException {
+ if (DEBUG) startTag("<%s>", TAG_MERGE);
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_MERGE.equals(tag)) {
+ if (row == null) {
+ parseKeyboardContent(parser, skip);
+ } else {
+ parseRowContent(parser, row, skip);
+ }
+ break;
+ } else {
+ throw new XmlParseUtils.ParseException(
+ "Included keyboard layout must have <merge> root element", parser);
+ }
+ }
+ }
+ }
+
+ private void parseSwitchKeyboardContent(final XmlPullParser parser, final boolean skip)
+ throws XmlPullParserException, IOException {
+ parseSwitchInternal(parser, null, skip);
+ }
+
+ private void parseSwitchRowContent(final XmlPullParser parser, final KeyboardRow row,
+ final boolean skip) throws XmlPullParserException, IOException {
+ parseSwitchInternal(parser, row, skip);
+ }
+
+ private void parseSwitchInternal(final XmlPullParser parser, final KeyboardRow row,
+ final boolean skip) throws XmlPullParserException, IOException {
+ if (DEBUG) startTag("<%s> %s", TAG_SWITCH, mParams.mId);
+ boolean selected = false;
+ int event;
+ while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG) {
+ final String tag = parser.getName();
+ if (TAG_CASE.equals(tag)) {
+ selected |= parseCase(parser, row, selected ? true : skip);
+ } else if (TAG_DEFAULT.equals(tag)) {
+ selected |= parseDefault(parser, row, selected ? true : skip);
+ } else {
+ throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY);
+ }
+ } else if (event == XmlPullParser.END_TAG) {
+ final String tag = parser.getName();
+ if (TAG_SWITCH.equals(tag)) {
+ if (DEBUG) endTag("</%s>", TAG_SWITCH);
+ break;
+ } else {
+ throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY);
+ }
+ }
+ }
+ }
+
+ private boolean parseCase(final XmlPullParser parser, final KeyboardRow row, final boolean skip)
+ throws XmlPullParserException, IOException {
+ final boolean selected = parseCaseCondition(parser);
+ if (row == null) {
+ // Processing Rows.
+ parseKeyboardContent(parser, selected ? skip : true);
+ } else {
+ // Processing Keys.
+ parseRowContent(parser, row, selected ? skip : true);
+ }
+ return selected;
+ }
+
+ private boolean parseCaseCondition(final XmlPullParser parser) {
+ final KeyboardId id = mParams.mId;
+ if (id == null) {
+ return true;
+ }
+ final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Case);
+ try {
+ final boolean keyboardLayoutSetElementMatched = matchTypedValue(a,
+ R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
+ KeyboardId.elementIdToName(id.mElementId));
+ final boolean modeMatched = matchTypedValue(a,
+ R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode));
+ final boolean navigateNextMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_navigateNext, id.navigateNext());
+ final boolean navigatePreviousMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_navigatePrevious, id.navigatePrevious());
+ final boolean passwordInputMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_passwordInput, id.passwordInput());
+ final boolean clobberSettingsKeyMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey);
+ final boolean shortcutKeyEnabledMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled);
+ final boolean hasShortcutKeyMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey);
+ final boolean languageSwitchKeyEnabledMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
+ id.mLanguageSwitchKeyEnabled);
+ final boolean isMultiLineMatched = matchBoolean(a,
+ R.styleable.Keyboard_Case_isMultiLine, id.isMultiLine());
+ final boolean imeActionMatched = matchInteger(a,
+ R.styleable.Keyboard_Case_imeAction, id.imeAction());
+ final boolean localeCodeMatched = matchString(a,
+ R.styleable.Keyboard_Case_localeCode, id.mLocale.toString());
+ final boolean languageCodeMatched = matchString(a,
+ R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
+ final boolean countryCodeMatched = matchString(a,
+ R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
+ final boolean selected = keyboardLayoutSetElementMatched && modeMatched
+ && navigateNextMatched && navigatePreviousMatched && passwordInputMatched
+ && clobberSettingsKeyMatched && shortcutKeyEnabledMatched
+ && 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", TAG_CASE,
+ textAttr(a.getString(
+ R.styleable.Keyboard_Case_keyboardLayoutSetElement),
+ "keyboardLayoutSetElement"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_imeAction),
+ "imeAction"),
+ booleanAttr(a, R.styleable.Keyboard_Case_navigateNext,
+ "navigateNext"),
+ booleanAttr(a, R.styleable.Keyboard_Case_navigatePrevious,
+ "navigatePrevious"),
+ booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey,
+ "clobberSettingsKey"),
+ booleanAttr(a, R.styleable.Keyboard_Case_passwordInput,
+ "passwordInput"),
+ booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled,
+ "shortcutKeyEnabled"),
+ booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey,
+ "hasShortcutKey"),
+ booleanAttr(a, R.styleable.Keyboard_Case_languageSwitchKeyEnabled,
+ "languageSwitchKeyEnabled"),
+ booleanAttr(a, R.styleable.Keyboard_Case_isMultiLine,
+ "isMultiLine"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_localeCode),
+ "localeCode"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_languageCode),
+ "languageCode"),
+ textAttr(a.getString(R.styleable.Keyboard_Case_countryCode),
+ "countryCode"),
+ selected ? "" : " skipped");
+ }
+
+ return selected;
+ } finally {
+ a.recycle();
+ }
+ }
+
+ private static boolean matchInteger(final TypedArray a, final int index, final int value) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for
+ // the attribute.
+ return !a.hasValue(index) || a.getInt(index, 0) == value;
+ }
+
+ private static boolean matchBoolean(final TypedArray a, final int index, final boolean value) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for
+ // the attribute.
+ return !a.hasValue(index) || a.getBoolean(index, false) == value;
+ }
+
+ private static boolean matchString(final TypedArray a, final int index, final String value) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for
+ // the attribute.
+ return !a.hasValue(index)
+ || StringUtils.containsInArray(value, a.getString(index).split("\\|"));
+ }
+
+ private static boolean matchTypedValue(final TypedArray a, final int index, final int intValue,
+ final String strValue) {
+ // If <case> does not have "index" attribute, that means this <case> is wild-card for
+ // the attribute.
+ final TypedValue v = a.peekValue(index);
+ if (v == null) {
+ return true;
+ }
+ if (ResourceUtils.isIntegerValue(v)) {
+ return intValue == a.getInt(index, 0);
+ } else if (ResourceUtils.isStringValue(v)) {
+ return StringUtils.containsInArray(strValue, a.getString(index).split("\\|"));
+ }
+ return false;
+ }
+
+ private boolean parseDefault(final XmlPullParser parser, final KeyboardRow row,
+ final boolean skip) throws XmlPullParserException, IOException {
+ if (DEBUG) startTag("<%s>", TAG_DEFAULT);
+ if (row == null) {
+ parseKeyboardContent(parser, skip);
+ } else {
+ parseRowContent(parser, row, skip);
+ }
+ return true;
+ }
+
+ 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);
+ try {
+ if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) {
+ throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE
+ + "/> needs styleName attribute", parser);
+ }
+ if (DEBUG) {
+ startEndTag("<%s styleName=%s />%s", TAG_KEY_STYLE,
+ keyStyleAttr.getString(R.styleable.Keyboard_KeyStyle_styleName),
+ skip ? " skipped" : "");
+ }
+ if (!skip) {
+ mParams.mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser);
+ }
+ } finally {
+ keyStyleAttr.recycle();
+ keyAttrs.recycle();
+ }
+ XmlParseUtils.checkEndTag(TAG_KEY_STYLE, parser);
+ }
+
+ private void startKeyboard() {
+ mCurrentY += mParams.mTopPadding;
+ mTopEdge = true;
+ }
+
+ private void startRow(final KeyboardRow row) {
+ addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
+ mCurrentRow = row;
+ mLeftEdge = true;
+ mRightEdgeKey = null;
+ }
+
+ private void endRow(final KeyboardRow row) {
+ if (mCurrentRow == null) {
+ throw new InflateException("orphan end row tag");
+ }
+ if (mRightEdgeKey != null) {
+ mRightEdgeKey.markAsRightEdge(mParams);
+ mRightEdgeKey = null;
+ }
+ addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
+ mCurrentY += row.mRowHeight;
+ mCurrentRow = null;
+ mTopEdge = false;
+ }
+
+ private void endKey(final Key key) {
+ mParams.onAddKey(key);
+ if (mLeftEdge) {
+ key.markAsLeftEdge(mParams);
+ mLeftEdge = false;
+ }
+ if (mTopEdge) {
+ key.markAsTopEdge(mParams);
+ }
+ mRightEdgeKey = key;
+ }
+
+ private void endKeyboard() {
+ // nothing to do here.
+ }
+
+ private void addEdgeSpace(final float width, final KeyboardRow row) {
+ row.advanceXPos(width);
+ mLeftEdge = false;
+ mRightEdgeKey = null;
+ }
+
+ private static String textAttr(final String value, final String name) {
+ return value != null ? String.format(" %s=%s", name, value) : "";
+ }
+
+ private static String booleanAttr(final TypedArray a, final int index, final String name) {
+ return a.hasValue(index)
+ ? String.format(" %s=%s", name, a.getBoolean(index, false)) : "";
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
index 67cb74f4d..840d7133d 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java
@@ -17,13 +17,13 @@
package com.android.inputmethod.keyboard.internal;
import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.CollectionUtils;
import java.util.HashMap;
-public class KeyboardCodesSet {
- private static final HashMap<String, int[]> sLanguageToCodesMap =
- new HashMap<String, int[]>();
- private static final HashMap<String, Integer> sNameToIdMap = new HashMap<String, Integer>();
+public final class KeyboardCodesSet {
+ private static final HashMap<String, int[]> sLanguageToCodesMap = CollectionUtils.newHashMap();
+ private static final HashMap<String, Integer> sNameToIdMap = CollectionUtils.newHashMap();
private int[] mCodes = DEFAULT;
@@ -52,6 +52,7 @@ public class KeyboardCodesSet {
"key_action_next",
"key_action_previous",
"key_language_switch",
+ "key_research",
"key_unspecified",
"key_left_parenthesis",
"key_right_parenthesis",
@@ -86,6 +87,7 @@ public class KeyboardCodesSet {
Keyboard.CODE_ACTION_NEXT,
Keyboard.CODE_ACTION_PREVIOUS,
Keyboard.CODE_LANGUAGE_SWITCH,
+ Keyboard.CODE_RESEARCH,
Keyboard.CODE_UNSPECIFIED,
CODE_LEFT_PARENTHESIS,
CODE_RIGHT_PARENTHESIS,
@@ -112,6 +114,7 @@ public class KeyboardCodesSet {
DEFAULT[11],
DEFAULT[12],
DEFAULT[13],
+ DEFAULT[14],
CODE_RIGHT_PARENTHESIS,
CODE_LEFT_PARENTHESIS,
CODE_GREATER_THAN_SIGN,
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
index 540e63b3f..7292e8e19 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
@@ -20,22 +20,23 @@ import android.content.res.Resources;
import android.content.res.TypedArray;
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 java.util.HashMap;
-public class KeyboardIconsSet {
+public final class KeyboardIconsSet {
private static final String TAG = KeyboardIconsSet.class.getSimpleName();
public static final int ICON_UNDEFINED = 0;
private static final int ATTR_UNDEFINED = 0;
- private static final HashMap<Integer, Integer> ATTR_ID_TO_ICON_ID
- = new HashMap<Integer, Integer>();
+ private static final SparseIntArray ATTR_ID_TO_ICON_ID = new SparseIntArray();
// Icon name to icon id map.
- private static final HashMap<String, Integer> sNameToIdsMap = new HashMap<String, Integer>();
+ private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap();
private static final Object[] NAMES_AND_ATTR_IDS = {
"undefined", ATTR_UNDEFINED,
@@ -76,7 +77,9 @@ public class KeyboardIconsSet {
}
public void loadIcons(final TypedArray keyboardAttrs) {
- for (final Integer attrId : ATTR_ID_TO_ICON_ID.keySet()) {
+ final int size = ATTR_ID_TO_ICON_ID.size();
+ for (int index = 0; index < size; index++) {
+ final int attrId = ATTR_ID_TO_ICON_ID.keyAt(index);
try {
final Drawable icon = keyboardAttrs.getDrawable(attrId);
setDefaultBounds(icon);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
new file mode 100644
index 000000000..e6fe50e02
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
@@ -0,0 +1,137 @@
+/*
+ * 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.keyboard.internal;
+
+import android.util.SparseIntArray;
+
+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 java.util.ArrayList;
+import java.util.TreeSet;
+
+public class KeyboardParams {
+ public KeyboardId mId;
+ public int mThemeId;
+
+ /** Total height and width of the keyboard, including the paddings and keys */
+ public int mOccupiedHeight;
+ public int mOccupiedWidth;
+
+ /** Base height and width of the keyboard used to calculate rows' or keys' heights and
+ * widths
+ */
+ public int mBaseHeight;
+ public int mBaseWidth;
+
+ public int mTopPadding;
+ public int mBottomPadding;
+ public int mHorizontalEdgesPadding;
+ public int mHorizontalCenterPadding;
+
+ public KeyVisualAttributes mKeyVisualAttributes;
+
+ public int mDefaultRowHeight;
+ public int mDefaultKeyWidth;
+ public int mHorizontalGap;
+ public int mVerticalGap;
+
+ public int mMoreKeysTemplate;
+ public int mMaxMoreKeysKeyboardColumn;
+
+ public int GRID_WIDTH;
+ public int GRID_HEIGHT;
+
+ public final TreeSet<Key> mKeys = CollectionUtils.newTreeSet(); // ordered set
+ public final ArrayList<Key> mShiftKeys = CollectionUtils.newArrayList();
+ public final ArrayList<Key> mAltCodeKeysWhileTyping = CollectionUtils.newArrayList();
+ public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
+ public final KeyboardCodesSet mCodesSet = new KeyboardCodesSet();
+ public final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
+ public final KeyStylesSet mKeyStyles = new KeyStylesSet(mTextsSet);
+
+ public KeysCache mKeysCache;
+
+ public int mMostCommonKeyHeight = 0;
+ public int mMostCommonKeyWidth = 0;
+
+ public boolean mProximityCharsCorrectionEnabled;
+
+ public final TouchPositionCorrection mTouchPositionCorrection =
+ new TouchPositionCorrection();
+
+ protected void clearKeys() {
+ mKeys.clear();
+ mShiftKeys.clear();
+ clearHistogram();
+ }
+
+ 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);
+ }
+ if (key.mCode == Keyboard.CODE_SHIFT) {
+ mShiftKeys.add(key);
+ }
+ if (key.altCodeWhileTyping()) {
+ mAltCodeKeysWhileTyping.add(key);
+ }
+ }
+
+ private int mMaxHeightCount = 0;
+ private int mMaxWidthCount = 0;
+ private final SparseIntArray mHeightHistogram = new SparseIntArray();
+ private final SparseIntArray mWidthHistogram = new SparseIntArray();
+
+ private void clearHistogram() {
+ mMostCommonKeyHeight = 0;
+ mMaxHeightCount = 0;
+ mHeightHistogram.clear();
+
+ mMaxWidthCount = 0;
+ mMostCommonKeyWidth = 0;
+ mWidthHistogram.clear();
+ }
+
+ private static int updateHistogramCounter(final SparseIntArray histogram, final int key) {
+ final int index = histogram.indexOfKey(key);
+ final int count = (index >= 0 ? histogram.get(key) : 0) + 1;
+ histogram.put(key, count);
+ return count;
+ }
+
+ private void updateHistogram(final Key key) {
+ final int height = key.mHeight + mVerticalGap;
+ final int heightCount = updateHistogramCounter(mHeightHistogram, height);
+ if (heightCount > mMaxHeightCount) {
+ mMaxHeightCount = heightCount;
+ mMostCommonKeyHeight = height;
+ }
+
+ final int width = key.mWidth + mHorizontalGap;
+ final int widthCount = updateHistogramCounter(mWidthHistogram, width);
+ if (widthCount > mMaxWidthCount) {
+ mMaxWidthCount = widthCount;
+ mMostCommonKeyWidth = width;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
new file mode 100644
index 000000000..b986262d7
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
@@ -0,0 +1,154 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+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 org.xmlpull.v1.XmlPullParser;
+
+/**
+ * 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}
+ * defines.
+ */
+public final class KeyboardRow {
+ // keyWidth enum constants
+ private static final int KEYWIDTH_NOT_ENUM = 0;
+ 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;
+
+ 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) {
+ mParams = params;
+ TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard);
+ mRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
+ R.styleable.Keyboard_rowHeight,
+ params.mBaseHeight, params.mDefaultRowHeight);
+ keyboardAttr.recycle();
+ TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+ R.styleable.Keyboard_Key);
+ mDefaultKeyWidth = ResourceUtils.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyWidth,
+ params.mBaseWidth, params.mDefaultKeyWidth);
+ mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
+ Key.BACKGROUND_TYPE_NORMAL);
+ 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 void setDefaultKeyWidth(final float defaultKeyWidth) {
+ mDefaultKeyWidth = defaultKeyWidth;
+ }
+
+ public int getDefaultKeyLabelFlags() {
+ return mDefaultKeyLabelFlags;
+ }
+
+ public void setDefaultKeyLabelFlags(final int keyLabelFlags) {
+ mDefaultKeyLabelFlags = keyLabelFlags;
+ }
+
+ public int getDefaultBackgroundType() {
+ return mDefaultBackgroundType;
+ }
+
+ public void setDefaultBackgroundType(final int backgroundType) {
+ mDefaultBackgroundType = backgroundType;
+ }
+
+ public void setXPos(final float keyXPos) {
+ mCurrentX = keyXPos;
+ }
+
+ public void advanceXPos(final float width) {
+ mCurrentX += width;
+ }
+
+ public int getKeyY() {
+ return mCurrentY;
+ }
+
+ public float getKeyX(final TypedArray keyAttr) {
+ final int keyboardRightEdge = mParams.mOccupiedWidth
+ - mParams.mHorizontalEdgesPadding;
+ if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
+ final float keyXPos = ResourceUtils.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyXPos, 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.
+ return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
+ } else {
+ return keyXPos + mParams.mHorizontalEdgesPadding;
+ }
+ }
+ return mCurrentX;
+ }
+
+ public float getKeyWidth(final TypedArray keyAttr) {
+ return getKeyWidth(keyAttr, mCurrentX);
+ }
+
+ public float getKeyWidth(final TypedArray keyAttr, final float keyXPos) {
+ final int widthType = ResourceUtils.getEnumValue(keyAttr,
+ R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
+ switch (widthType) {
+ case KEYWIDTH_FILL_RIGHT:
+ final int keyboardRightEdge =
+ mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
+ // If keyWidth is fillRight, the actual key width will be determined to fill
+ // out the area up to the right edge of the keyboard.
+ return keyboardRightEdge - keyXPos;
+ default: // KEYWIDTH_NOT_ENUM
+ return ResourceUtils.getDimensionOrFraction(keyAttr,
+ R.styleable.Keyboard_Key_keyWidth,
+ mParams.mBaseWidth, mDefaultKeyWidth);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index 43ffb85f7..58cc8972a 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -21,8 +21,6 @@ import android.util.Log;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.ResearchLogger;
-import com.android.inputmethod.latin.define.ProductionFlag;
/**
* Keyboard state machine.
@@ -36,7 +34,7 @@ import com.android.inputmethod.latin.define.ProductionFlag;
*
* The actions are {@link SwitchActions}'s methods.
*/
-public class KeyboardState {
+public final class KeyboardState {
private static final String TAG = KeyboardState.class.getSimpleName();
private static final boolean DEBUG_EVENT = false;
private static final boolean DEBUG_ACTION = false;
@@ -94,7 +92,7 @@ public class KeyboardState {
private final SavedKeyboardState mSavedKeyboardState = new SavedKeyboardState();
- static class SavedKeyboardState {
+ static final class SavedKeyboardState {
public boolean mIsValid;
public boolean mIsAlphabetMode;
public boolean mIsAlphabetShiftLocked;
@@ -305,9 +303,6 @@ public class KeyboardState {
Log.d(TAG, "onPressKey: code=" + Keyboard.printableCode(code)
+ " single=" + isSinglePointer + " autoCaps=" + autoCaps + " " + this);
}
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.keyboardState_onPressKey(code, this);
- }
if (code == Keyboard.CODE_SHIFT) {
onPressShift();
} else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
@@ -341,9 +336,6 @@ public class KeyboardState {
Log.d(TAG, "onReleaseKey: code=" + Keyboard.printableCode(code)
+ " sliding=" + withSliding + " " + this);
}
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.keyboardState_onReleaseKey(this, code, withSliding);
- }
if (code == Keyboard.CODE_SHIFT) {
onReleaseShift(withSliding);
} else if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) {
@@ -375,9 +367,6 @@ public class KeyboardState {
if (DEBUG_EVENT) {
Log.d(TAG, "onLongPressTimeout: code=" + Keyboard.printableCode(code) + " " + this);
}
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.keyboardState_onLongPressTimeout(code, this);
- }
if (mIsAlphabetMode && code == Keyboard.CODE_SHIFT) {
mLongPressShiftLockFired = true;
mSwitchActions.hapticAndAudioFeedback(code);
@@ -509,9 +498,6 @@ public class KeyboardState {
if (DEBUG_EVENT) {
Log.d(TAG, "onCancelInput: single=" + isSinglePointer + " " + this);
}
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.keyboardState_onCancelInput(isSinglePointer, this);
- }
// Switch back to the previous keyboard mode if the user cancels sliding input.
if (isSinglePointer) {
if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
@@ -543,9 +529,6 @@ public class KeyboardState {
+ " single=" + isSinglePointer
+ " autoCaps=" + autoCaps + " " + this);
}
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.keyboardState_onCodeInput(code, isSinglePointer, autoCaps, this);
- }
switch (mSwitchState) {
case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index d762797dd..3bb272f8c 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -19,6 +19,7 @@ package com.android.inputmethod.keyboard.internal;
import android.content.Context;
import android.content.res.Resources;
+import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.R;
import java.util.HashMap;
@@ -45,14 +46,12 @@ import java.util.HashMap;
*/
public final class KeyboardTextsSet {
// Language to texts map.
- private static final HashMap<String, String[]> sLocaleToTextsMap =
- new HashMap<String, String[]>();
- private static final HashMap<String, Integer> sNameToIdsMap =
- new HashMap<String, Integer>();
+ private static final HashMap<String, String[]> sLocaleToTextsMap = CollectionUtils.newHashMap();
+ private static final HashMap<String, Integer> sNameToIdsMap = CollectionUtils.newHashMap();
private String[] mTexts;
// Resource name to text map.
- private HashMap<String, String> mResourceNameToTextsMap = new HashMap<String, String>();
+ private HashMap<String, String> mResourceNameToTextsMap = CollectionUtils.newHashMap();
public void setLanguage(final String language) {
mTexts = sLocaleToTextsMap.get(language);
@@ -131,71 +130,71 @@ public final class KeyboardTextsSet {
/* 23 */ "more_keys_for_nordic_row2_10",
/* 24 */ "more_keys_for_nordic_row2_11",
/* 25 */ "keylabel_for_east_slavic_row1_9",
- /* 26 */ "keylabel_for_east_slavic_row2_1",
- /* 27 */ "keylabel_for_east_slavic_row3_5",
- /* 28 */ "more_keys_for_cyrillic_u",
- /* 29 */ "more_keys_for_cyrillic_ye",
- /* 30 */ "more_keys_for_cyrillic_en",
- /* 31 */ "more_keys_for_cyrillic_ha",
- /* 32 */ "more_keys_for_east_slavic_row2_1",
- /* 33 */ "more_keys_for_cyrillic_o",
- /* 34 */ "more_keys_for_cyrillic_soft_sign",
- /* 35 */ "keylabel_for_south_slavic_row1_6",
- /* 36 */ "keylabel_for_south_slavic_row2_11",
- /* 37 */ "keylabel_for_south_slavic_row3_1",
- /* 38 */ "keylabel_for_south_slavic_row3_8",
- /* 39 */ "more_keys_for_cyrillic_ie",
- /* 40 */ "more_keys_for_cyrillic_i",
- /* 41 */ "more_keys_for_single_quote",
- /* 42 */ "more_keys_for_double_quote",
- /* 43 */ "more_keys_for_tablet_double_quote",
- /* 44 */ "more_keys_for_currency_dollar",
- /* 45 */ "more_keys_for_currency_euro",
- /* 46 */ "more_keys_for_currency_pound",
- /* 47 */ "more_keys_for_currency_general",
- /* 48 */ "more_keys_for_punctuation",
- /* 49 */ "more_keys_for_star",
- /* 50 */ "more_keys_for_bullet",
- /* 51 */ "more_keys_for_plus",
- /* 52 */ "more_keys_for_left_parenthesis",
- /* 53 */ "more_keys_for_right_parenthesis",
- /* 54 */ "more_keys_for_less_than",
- /* 55 */ "more_keys_for_greater_than",
- /* 56 */ "more_keys_for_arabic_diacritics",
- /* 57 */ "keyhintlabel_for_arabic_diacritics",
- /* 58 */ "keylabel_for_symbols_1",
- /* 59 */ "keylabel_for_symbols_2",
- /* 60 */ "keylabel_for_symbols_3",
- /* 61 */ "keylabel_for_symbols_4",
- /* 62 */ "keylabel_for_symbols_5",
- /* 63 */ "keylabel_for_symbols_6",
- /* 64 */ "keylabel_for_symbols_7",
- /* 65 */ "keylabel_for_symbols_8",
- /* 66 */ "keylabel_for_symbols_9",
- /* 67 */ "keylabel_for_symbols_0",
- /* 68 */ "additional_more_keys_for_symbols_1",
- /* 69 */ "additional_more_keys_for_symbols_2",
- /* 70 */ "additional_more_keys_for_symbols_3",
- /* 71 */ "additional_more_keys_for_symbols_4",
- /* 72 */ "additional_more_keys_for_symbols_5",
- /* 73 */ "additional_more_keys_for_symbols_6",
- /* 74 */ "additional_more_keys_for_symbols_7",
- /* 75 */ "additional_more_keys_for_symbols_8",
- /* 76 */ "additional_more_keys_for_symbols_9",
- /* 77 */ "additional_more_keys_for_symbols_0",
- /* 78 */ "more_keys_for_symbols_1",
- /* 79 */ "more_keys_for_symbols_2",
- /* 80 */ "more_keys_for_symbols_3",
- /* 81 */ "more_keys_for_symbols_4",
- /* 82 */ "more_keys_for_symbols_5",
- /* 83 */ "more_keys_for_symbols_6",
- /* 84 */ "more_keys_for_symbols_7",
- /* 85 */ "more_keys_for_symbols_8",
- /* 86 */ "more_keys_for_symbols_9",
- /* 87 */ "more_keys_for_symbols_0",
- /* 88 */ "keylabel_for_comma",
- /* 89 */ "more_keys_for_comma",
- /* 90 */ "keylabel_for_symbols_exclamation",
+ /* 26 */ "keylabel_for_east_slavic_row1_12",
+ /* 27 */ "keylabel_for_east_slavic_row2_1",
+ /* 28 */ "keylabel_for_east_slavic_row2_11",
+ /* 29 */ "keylabel_for_east_slavic_row3_5",
+ /* 30 */ "more_keys_for_cyrillic_u",
+ /* 31 */ "more_keys_for_cyrillic_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 */ "more_keys_for_single_quote",
+ /* 43 */ "more_keys_for_double_quote",
+ /* 44 */ "more_keys_for_tablet_double_quote",
+ /* 45 */ "more_keys_for_currency_dollar",
+ /* 46 */ "more_keys_for_currency_euro",
+ /* 47 */ "more_keys_for_currency_pound",
+ /* 48 */ "more_keys_for_currency_general",
+ /* 49 */ "more_keys_for_punctuation",
+ /* 50 */ "more_keys_for_star",
+ /* 51 */ "more_keys_for_bullet",
+ /* 52 */ "more_keys_for_plus",
+ /* 53 */ "more_keys_for_left_parenthesis",
+ /* 54 */ "more_keys_for_right_parenthesis",
+ /* 55 */ "more_keys_for_less_than",
+ /* 56 */ "more_keys_for_greater_than",
+ /* 57 */ "more_keys_for_arabic_diacritics",
+ /* 58 */ "keyhintlabel_for_arabic_diacritics",
+ /* 59 */ "keylabel_for_symbols_1",
+ /* 60 */ "keylabel_for_symbols_2",
+ /* 61 */ "keylabel_for_symbols_3",
+ /* 62 */ "keylabel_for_symbols_4",
+ /* 63 */ "keylabel_for_symbols_5",
+ /* 64 */ "keylabel_for_symbols_6",
+ /* 65 */ "keylabel_for_symbols_7",
+ /* 66 */ "keylabel_for_symbols_8",
+ /* 67 */ "keylabel_for_symbols_9",
+ /* 68 */ "keylabel_for_symbols_0",
+ /* 69 */ "additional_more_keys_for_symbols_1",
+ /* 70 */ "additional_more_keys_for_symbols_2",
+ /* 71 */ "additional_more_keys_for_symbols_3",
+ /* 72 */ "additional_more_keys_for_symbols_4",
+ /* 73 */ "additional_more_keys_for_symbols_5",
+ /* 74 */ "additional_more_keys_for_symbols_6",
+ /* 75 */ "additional_more_keys_for_symbols_7",
+ /* 76 */ "additional_more_keys_for_symbols_8",
+ /* 77 */ "additional_more_keys_for_symbols_9",
+ /* 78 */ "additional_more_keys_for_symbols_0",
+ /* 79 */ "more_keys_for_symbols_1",
+ /* 80 */ "more_keys_for_symbols_2",
+ /* 81 */ "more_keys_for_symbols_3",
+ /* 82 */ "more_keys_for_symbols_4",
+ /* 83 */ "more_keys_for_symbols_5",
+ /* 84 */ "more_keys_for_symbols_6",
+ /* 85 */ "more_keys_for_symbols_7",
+ /* 86 */ "more_keys_for_symbols_8",
+ /* 87 */ "more_keys_for_symbols_9",
+ /* 88 */ "more_keys_for_symbols_0",
+ /* 89 */ "keylabel_for_comma",
+ /* 90 */ "more_keys_for_comma",
/* 91 */ "keylabel_for_symbols_question",
/* 92 */ "keylabel_for_symbols_semicolon",
/* 93 */ "keylabel_for_symbols_percent",
@@ -211,22 +210,29 @@ public final class KeyboardTextsSet {
/* 103 */ "keylabel_for_apostrophe",
/* 104 */ "keyhintlabel_for_apostrophe",
/* 105 */ "more_keys_for_apostrophe",
- /* 106 */ "more_keys_for_am_pm",
- /* 107 */ "settings_as_more_key",
- /* 108 */ "shortcut_as_more_key",
- /* 109 */ "action_next_as_more_key",
- /* 110 */ "action_previous_as_more_key",
- /* 111 */ "label_to_more_symbol_key",
- /* 112 */ "label_to_more_symbol_for_tablet_key",
- /* 113 */ "label_tab_key",
- /* 114 */ "label_to_phone_numeric_key",
- /* 115 */ "label_to_phone_symbols_key",
- /* 116 */ "label_time_am",
- /* 117 */ "label_time_pm",
- /* 118 */ "label_to_symbol_key_pcqwerty",
- /* 119 */ "keylabel_for_popular_domain",
- /* 120 */ "more_keys_for_popular_domain",
- /* 121 */ "more_keys_for_smiley",
+ /* 106 */ "more_keys_for_q",
+ /* 107 */ "more_keys_for_x",
+ /* 108 */ "keylabel_for_q",
+ /* 109 */ "keylabel_for_w",
+ /* 110 */ "keylabel_for_y",
+ /* 111 */ "keylabel_for_x",
+ /* 112 */ "keylabel_for_spanish_row2_10",
+ /* 113 */ "more_keys_for_am_pm",
+ /* 114 */ "settings_as_more_key",
+ /* 115 */ "shortcut_as_more_key",
+ /* 116 */ "action_next_as_more_key",
+ /* 117 */ "action_previous_as_more_key",
+ /* 118 */ "label_to_more_symbol_key",
+ /* 119 */ "label_to_more_symbol_for_tablet_key",
+ /* 120 */ "label_tab_key",
+ /* 121 */ "label_to_phone_numeric_key",
+ /* 122 */ "label_to_phone_symbols_key",
+ /* 123 */ "label_time_am",
+ /* 124 */ "label_time_pm",
+ /* 125 */ "label_to_symbol_key_pcqwerty",
+ /* 126 */ "keylabel_for_popular_domain",
+ /* 127 */ "more_keys_for_popular_domain",
+ /* 128 */ "more_keys_for_smiley",
};
private static final String EMPTY = "";
@@ -237,41 +243,41 @@ 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,
- /* ~40 */
- /* 41 */ "!fixedColumnOrder!4,\u2018,\u2019,\u201A,\u201B",
+ EMPTY, EMPTY, EMPTY,
+ /* ~41 */
+ /* 42 */ "!fixedColumnOrder!4,\u2018,\u2019,\u201A,\u201B",
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
// <string name="more_keys_for_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;</string>
- /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB",
+ /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB",
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
// <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
- /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
+ /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
// U+00A2: "¢" CENT SIGN
// U+00A3: "£" POUND SIGN
// U+20AC: "€" EURO SIGN
// U+00A5: "¥" YEN SIGN
// U+20B1: "₱" PESO SIGN
- /* 44 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1",
- /* 45 */ "\u00A2,\u00A3,$,\u00A5,\u20B1",
- /* 46 */ "\u00A2,$,\u20AC,\u00A5,\u20B1",
- /* 47 */ "\u00A2,$,\u20AC,\u00A3,\u00A5,\u20B1",
- /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)",
+ /* 45 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1",
+ /* 46 */ "\u00A2,\u00A3,$,\u00A5,\u20B1",
+ /* 47 */ "\u00A2,$,\u20AC,\u00A5,\u20B1",
+ /* 48 */ "\u00A2,$,\u20AC,\u00A3,\u00A5,\u20B1",
+ /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)",
// U+2020: "†" DAGGER
// U+2021: "‡" DOUBLE DAGGER
// U+2605: "★" BLACK STAR
- /* 49 */ "\u2020,\u2021,\u2605",
+ /* 50 */ "\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
- /* 50 */ "\u266A,\u2665,\u2660,\u2666,\u2663",
+ /* 51 */ "\u266A,\u2665,\u2660,\u2666,\u2663",
// U+00B1: "±" PLUS-MINUS SIGN
- /* 51 */ "\u00B1",
+ /* 52 */ "\u00B1",
// The all letters need to be mirrored are found at
// http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
- /* 52 */ "!fixedColumnOrder!3,<,{,[",
- /* 53 */ "!fixedColumnOrder!3,>,},]",
+ /* 53 */ "!fixedColumnOrder!3,<,{,[",
+ /* 54 */ "!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
@@ -287,51 +293,50 @@ public final class KeyboardTextsSet {
// U+201D: "”" RIGHT DOUBLE QUOTATION MARK
// U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
// U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
- /* 54 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB",
- /* 55 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB",
- /* 56 */ EMPTY,
+ /* 55 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB",
+ /* 56 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB",
/* 57 */ EMPTY,
- /* 58 */ "1",
- /* 59 */ "2",
- /* 60 */ "3",
- /* 61 */ "4",
- /* 62 */ "5",
- /* 63 */ "6",
- /* 64 */ "7",
- /* 65 */ "8",
- /* 66 */ "9",
- /* 67 */ "0",
- /* 68~ */
+ /* 58 */ EMPTY,
+ /* 59 */ "1",
+ /* 60 */ "2",
+ /* 61 */ "3",
+ /* 62 */ "4",
+ /* 63 */ "5",
+ /* 64 */ "6",
+ /* 65 */ "7",
+ /* 66 */ "8",
+ /* 67 */ "9",
+ /* 68 */ "0",
+ /* 69~ */
EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
- /* ~77 */
+ /* ~78 */
// 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
- /* 78 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B",
+ /* 79 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B",
// U+00B2: "²" SUPERSCRIPT TWO
// U+2154: "⅔" VULGAR FRACTION TWO THIRDS
- /* 79 */ "\u00B2,\u2154",
+ /* 80 */ "\u00B2,\u2154",
// U+00B3: "³" SUPERSCRIPT THREE
// U+00BE: "¾" VULGAR FRACTION THREE QUARTERS
// U+215C: "⅜" VULGAR FRACTION THREE EIGHTHS
- /* 80 */ "\u00B3,\u00BE,\u215C",
+ /* 81 */ "\u00B3,\u00BE,\u215C",
// U+2074: "⁴" SUPERSCRIPT FOUR
- /* 81 */ "\u2074",
+ /* 82 */ "\u2074",
// U+215D: "⅝" VULGAR FRACTION FIVE EIGHTHS
- /* 82 */ "\u215D",
- /* 83 */ EMPTY,
+ /* 83 */ "\u215D",
+ /* 84 */ EMPTY,
// U+215E: "⅞" VULGAR FRACTION SEVEN EIGHTHS
- /* 84 */ "\u215E",
- /* 85 */ EMPTY,
+ /* 85 */ "\u215E",
/* 86 */ EMPTY,
+ /* 87 */ EMPTY,
// U+207F: "ⁿ" SUPERSCRIPT LATIN SMALL LETTER N
// U+2205: "∅" EMPTY SET
- /* 87 */ "\u207F,\u2205",
- /* 88 */ ",",
- /* 89 */ EMPTY,
- /* 90 */ "!",
+ /* 88 */ "\u207F,\u2205",
+ /* 89 */ ",",
+ /* 90 */ EMPTY,
/* 91 */ "?",
/* 92 */ ";",
/* 93 */ "%",
@@ -350,33 +355,94 @@ public final class KeyboardTextsSet {
/* 103 */ "\'",
/* 104 */ "\"",
/* 105 */ "\"",
- /* 106 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm",
- /* 107 */ "!icon/settings_key|!code/key_settings",
- /* 108 */ "!icon/shortcut_key|!code/key_shortcut",
- /* 109 */ "!hasLabels!,!text/label_next_key|!code/key_action_next",
- /* 110 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous",
+ /* 106 */ EMPTY,
+ /* 107 */ EMPTY,
+ /* 108 */ "q",
+ /* 109 */ "w",
+ /* 110 */ "y",
+ /* 111 */ "x",
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ /* 112 */ "\u00F1",
+ /* 113 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm",
+ /* 114 */ "!icon/settings_key|!code/key_settings",
+ /* 115 */ "!icon/shortcut_key|!code/key_shortcut",
+ /* 116 */ "!hasLabels!,!text/label_next_key|!code/key_action_next",
+ /* 117 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous",
// Label for "switch to more symbol" modifier key. Must be short to fit on key!
- /* 111 */ "= \\ <",
+ /* 118 */ "= \\ <",
// Label for "switch to more symbol" modifier key on tablets. Must be short to fit on key!
- /* 112 */ "~ \\ {",
+ /* 119 */ "~ \\ {",
// Label for "Tab" key. Must be short to fit on key!
- /* 113 */ "Tab",
+ /* 120 */ "Tab",
// Label for "switch to phone numeric" key. Must be short to fit on key!
- /* 114 */ "123",
+ /* 121 */ "123",
// Label for "switch to phone symbols" key. Must be short to fit on key!
// U+FF0A: "*" FULLWIDTH ASTERISK
// U+FF03: "#" FULLWIDTH NUMBER SIGN
- /* 115 */ "\uFF0A\uFF03",
+ /* 122 */ "\uFF0A\uFF03",
// Key label for "ante meridiem"
- /* 116 */ "AM",
+ /* 123 */ "AM",
// Key label for "post meridiem"
- /* 117 */ "PM",
+ /* 124 */ "PM",
// Label for "switch to symbols" key on PC QWERTY layout
- /* 118 */ "Sym",
- /* 119 */ ".com",
+ /* 125 */ "Sym",
+ /* 126 */ ".com",
// popular web domains for the locale - most popular, displayed on the keyboard
- /* 120 */ "!hasLabels!,.net,.org,.gov,.edu",
- /* 121 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ",
+ /* 127 */ "!hasLabels!,.net,.org,.gov,.edu",
+ /* 128 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ",
+ };
+
+ /* Language af: Afrikaans */
+ private static final String[] LANGUAGE_af = {
+ // This is the same as Dutch except more keys of y and demoting vowels with diaeresis.
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E1,\u00E2,\u00E4,\u00E0,\u00E6,\u00E3,\u00E5,\u0101",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E9,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ // U+0133: "ij" LATIN SMALL LIGATURE IJ
+ /* 2 */ "\u00ED,\u00EC,\u00EF,\u00EE,\u012F,\u012B,\u0133",
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ /* 3 */ "\u00F3,\u00F4,\u00F6,\u00F2,\u00F5,\u0153,\u00F8,\u014D",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FA,\u00FB,\u00FC,\u00F9,\u016B",
+ /* 5 */ null,
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u00F1,\u0144",
+ /* 7 */ null,
+ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+ // U+0133: "ij" LATIN SMALL LIGATURE IJ
+ /* 8 */ "\u00FD,\u0133",
};
/* Language ar: Arabic */
@@ -384,33 +450,33 @@ 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,
+ /* ~42 */
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
// <string name="more_keys_for_double_quote">&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB;|&#x00AB;</string>
- /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB",
+ /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB",
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
// <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB|&#x00AB;;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
- /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
- /* 44~ */
+ /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
+ /* 45~ */
null, null, null, null,
- /* ~47 */
+ /* ~48 */
// U+061F: "؟" ARABIC QUESTION MARK
// U+060C: "،" ARABIC COMMA
// U+061B: "؛" ARABIC SEMICOLON
- /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
+ /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
// U+2605: "★" BLACK STAR
// U+066D: "٭" ARABIC FIVE POINTED STAR
- /* 49 */ "\u2605,\u066D",
+ /* 50 */ "\u2605,\u066D",
// U+266A: "♪" EIGHTH NOTE
- /* 50 */ "\u266A",
- /* 51 */ null,
+ /* 51 */ "\u266A",
+ /* 52 */ 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
- /* 52 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
- /* 53 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[",
+ /* 53 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
+ /* 54 */ "!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
@@ -426,8 +492,8 @@ public final class KeyboardTextsSet {
// U+201D: "”" RIGHT DOUBLE QUOTATION MARK
// U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
// U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
- /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
- /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
+ /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
+ /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
// U+0655: "ٕ" ARABIC HAMZA BELOW
// U+0654: "ٔ" ARABIC HAMZA ABOVE
// U+0652: "ْ" ARABIC SUKUN
@@ -443,47 +509,46 @@ public final class KeyboardTextsSet {
// U+064E: "َ" ARABIC FATHA
// U+0640: "ـ" ARABIC TATWEEL
// In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label.
- /* 56 */ "!fixedColumnOrder!7,\u0655,\u0654,\u0652,\u064D,\u064C,\u064B,\u0651,\u0656,\u0670,\u0653,\u0650,\u064F,\u064E,\u0640\u0640\u0640|\u0640",
- /* 57 */ "\u0651",
+ /* 57 */ "!fixedColumnOrder!7,\u0655,\u0654,\u0652,\u064D,\u064C,\u064B,\u0651,\u0656,\u0670,\u0653,\u0650,\u064F,\u064E,\u0640\u0640\u0640|\u0640",
+ /* 58 */ "\u0651",
// U+0661: "١" ARABIC-INDIC DIGIT ONE
- /* 58 */ "\u0661",
+ /* 59 */ "\u0661",
// U+0662: "٢" ARABIC-INDIC DIGIT TWO
- /* 59 */ "\u0662",
+ /* 60 */ "\u0662",
// U+0663: "٣" ARABIC-INDIC DIGIT THREE
- /* 60 */ "\u0663",
+ /* 61 */ "\u0663",
// U+0664: "٤" ARABIC-INDIC DIGIT FOUR
- /* 61 */ "\u0664",
+ /* 62 */ "\u0664",
// U+0665: "٥" ARABIC-INDIC DIGIT FIVE
- /* 62 */ "\u0665",
+ /* 63 */ "\u0665",
// U+0666: "٦" ARABIC-INDIC DIGIT SIX
- /* 63 */ "\u0666",
+ /* 64 */ "\u0666",
// U+0667: "٧" ARABIC-INDIC DIGIT SEVEN
- /* 64 */ "\u0667",
+ /* 65 */ "\u0667",
// U+0668: "٨" ARABIC-INDIC DIGIT EIGHT
- /* 65 */ "\u0668",
+ /* 66 */ "\u0668",
// U+0669: "٩" ARABIC-INDIC DIGIT NINE
- /* 66 */ "\u0669",
+ /* 67 */ "\u0669",
// U+0660: "٠" ARABIC-INDIC DIGIT ZERO
- /* 67 */ "\u0660",
- /* 68 */ "1",
- /* 69 */ "2",
- /* 70 */ "3",
- /* 71 */ "4",
- /* 72 */ "5",
- /* 73 */ "6",
- /* 74 */ "7",
- /* 75 */ "8",
- /* 76 */ "9",
+ /* 68 */ "\u0660",
+ /* 69 */ "1",
+ /* 70 */ "2",
+ /* 71 */ "3",
+ /* 72 */ "4",
+ /* 73 */ "5",
+ /* 74 */ "6",
+ /* 75 */ "7",
+ /* 76 */ "8",
+ /* 77 */ "9",
// U+066B: "٫" ARABIC DECIMAL SEPARATOR
// U+066C: "٬" ARABIC THOUSANDS SEPARATOR
- /* 77 */ "0,\u066B,\u066C",
- /* 78~ */
+ /* 78 */ "0,\u066B,\u066C",
+ /* 79~ */
null, null, null, null, null, null, null, null, null, null,
- /* ~87 */
+ /* ~88 */
// U+060C: "،" ARABIC COMMA
- /* 88 */ "\u060C",
- /* 89 */ "\\,",
- /* 90 */ null,
+ /* 89 */ "\u060C",
+ /* 90 */ "\\,",
/* 91 */ "\u061F",
/* 92 */ "\u061B",
// U+066A: "٪" ARABIC PERCENT SIGN
@@ -512,19 +577,24 @@ public final class KeyboardTextsSet {
/* ~24 */
// U+045E: "ў" CYRILLIC SMALL LETTER SHORT U
/* 25 */ "\u045E",
+ // U+0451: "ё" CYRILLIC SMALL LETTER IO
+ /* 26 */ "\u0451",
// U+044B: "ы" CYRILLIC SMALL LETTER YERU
- /* 26 */ "\u044B",
+ /* 27 */ "\u044B",
+ // U+044D: "э" CYRILLIC SMALL LETTER E
+ /* 28 */ "\u044D",
// U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I
- /* 27 */ "\u0456",
- /* 28~ */
- null, null, null,
- /* ~30 */
- // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 31 */ "\u044A",
- /* 32 */ null,
- /* 33 */ null,
+ /* 29 */ "\u0456",
+ /* 30~ */
+ null, null, null, null, null,
+ /* ~34 */
// U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 34 */ "\u044A",
+ /* 35 */ "\u044A",
+ /* 36~ */
+ null, null, null, null,
+ /* ~39 */
+ // U+0451: "ё" CYRILLIC SMALL LETTER IO
+ /* 40 */ "\u0451",
};
/* Language ca: Catalan */
@@ -806,6 +876,144 @@ public final class KeyboardTextsSet {
/* 7 */ "\u00E7",
};
+ /* Language eo: Esperanto */
+ private static final String[] LANGUAGE_eo = {
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ // U+0103: "ă" LATIN SMALL LETTER A WITH BREVE
+ // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+ // U+00AA: "ª" FEMININE ORDINAL INDICATOR
+ /* 0 */ "\u00E1,\u00E0,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101,\u0103,\u0105,\u00AA",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+011B: "ě" LATIN SMALL LETTER E WITH CARON
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E9,\u011B,\u00E8,\u00EA,\u00EB,\u0119,\u0117,\u0113",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+0129: "ĩ" LATIN SMALL LETTER I WITH TILDE
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ // U+0131: "ı" LATIN SMALL LETTER DOTLESS I
+ // U+0133: "ij" LATIN SMALL LIGATURE IJ
+ /* 2 */ "\u00ED,\u00EE,\u00EF,\u0129,\u00EC,\u012F,\u012B,\u0131,\u0133",
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ // U+0151: "ő" LATIN SMALL LETTER O WITH DOUBLE ACUTE
+ // U+00BA: "º" MASCULINE ORDINAL INDICATOR
+ /* 3 */ "\u00F3,\u00F6,\u00F4,\u00F2,\u00F5,\u0153,\u00F8,\u014D,\u0151,\u00BA",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+016F: "ů" LATIN SMALL LETTER U WITH RING ABOVE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ // U+0169: "ũ" LATIN SMALL LETTER U WITH TILDE
+ // U+0171: "ű" LATIN SMALL LETTER U WITH DOUBLE ACUTE
+ // U+0173: "ų" LATIN SMALL LETTER U WITH OGONEK
+ // U+00B5: "µ" MICRO SIGN
+ /* 4 */ "\u00FA,\u016F,\u00FB,\u00FC,\u00F9,\u016B,\u0169,\u0171,\u0173,\u00B5",
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // U+0219: "ș" LATIN SMALL LETTER S WITH COMMA BELOW
+ // U+015F: "ş" LATIN SMALL LETTER S WITH CEDILLA
+ /* 5 */ "\u00DF,\u0161,\u015B,\u0219,\u015F",
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ // U+0146: "ņ" LATIN SMALL LETTER N WITH CEDILLA
+ // U+0148: "ň" LATIN SMALL LETTER N WITH CARON
+ // U+0149: "ʼn" LATIN SMALL LETTER N PRECEDED BY APOSTROPHE
+ // U+014B: "ŋ" LATIN SMALL LETTER ENG
+ /* 6 */ "\u00F1,\u0144,\u0146,\u0148,\u0149,\u014B",
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+010B: "ċ" LATIN SMALL LETTER C WITH DOT ABOVE
+ /* 7 */ "\u0107,\u010D,\u00E7,\u010B",
+ // U+00FD: "ý" LATIN SMALL LETTER Y WITH ACUTE
+ // U+0177: "ŷ" LATIN SMALL LETTER Y WITH CIRCUMFLEX
+ // U+00FF: "ÿ" LATIN SMALL LETTER Y WITH DIAERESIS
+ // U+00FE: "þ" LATIN SMALL LETTER THORN
+ /* 8 */ "y,\u00FD,\u0177,\u00FF,\u00FE",
+ // U+00F0: "ð" LATIN SMALL LETTER ETH
+ // U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+ // U+0111: "đ" LATIN SMALL LETTER D WITH STROKE
+ /* 9 */ "\u00F0,\u010F,\u0111",
+ // U+0159: "ř" LATIN SMALL LETTER R WITH CARON
+ // U+0155: "ŕ" LATIN SMALL LETTER R WITH ACUTE
+ // U+0157: "ŗ" LATIN SMALL LETTER R WITH CEDILLA
+ /* 10 */ "\u0159,\u0155,\u0157",
+ // U+0165: "ť" LATIN SMALL LETTER T WITH CARON
+ // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW
+ // U+0163: "ţ" LATIN SMALL LETTER T WITH CEDILLA
+ // U+0167: "ŧ" LATIN SMALL LETTER T WITH STROKE
+ /* 11 */ "\u0165,\u021B,\u0163,\u0167",
+ // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+ // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ /* 12 */ "\u017A,\u017C,\u017E",
+ // U+0137: "ķ" LATIN SMALL LETTER K WITH CEDILLA
+ // U+0138: "ĸ" LATIN SMALL LETTER KRA
+ /* 13 */ "\u0137,\u0138",
+ // U+013A: "ĺ" LATIN SMALL LETTER L WITH ACUTE
+ // U+013C: "ļ" LATIN SMALL LETTER L WITH CEDILLA
+ // U+013E: "ľ" LATIN SMALL LETTER L WITH CARON
+ // U+0140: "ŀ" LATIN SMALL LETTER L WITH MIDDLE DOT
+ // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE
+ /* 14 */ "\u013A,\u013C,\u013E,\u0140,\u0142",
+ // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE
+ // U+0121: "ġ" LATIN SMALL LETTER G WITH DOT ABOVE
+ // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA
+ /* 15 */ "\u011F,\u0121,\u0123",
+ // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX
+ /* 16 */ "w,\u0175",
+ // U+0125: "ĥ" LATIN SMALL LETTER H WITH CIRCUMFLEX
+ // U+0127: "ħ" LATIN SMALL LETTER H WITH STROKE
+ /* 17 */ "\u0125,\u0127",
+ /* 18 */ null,
+ // U+0175: "ŵ" LATIN SMALL LETTER W WITH CIRCUMFLEX
+ /* 19 */ "w,\u0175",
+ /* 20~ */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null,
+ /* ~105 */
+ /* 106 */ "q",
+ /* 107 */ "x",
+ // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX
+ /* 108 */ "\u015D",
+ // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX
+ /* 109 */ "\u011D",
+ // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE
+ /* 110 */ "\u016D",
+ // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX
+ /* 111 */ "\u0109",
+ // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX
+ /* 112 */ "\u0135",
+ };
+
/* Language es: Spanish */
private static final String[] LANGUAGE_es = {
// U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
@@ -861,31 +1069,22 @@ 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,
- /* ~47 */
+ null, null, null, null, null, null, null, null, null, null, null,
+ /* ~48 */
// U+00A1: "¡" INVERTED EXCLAMATION MARK
// U+00BF: "¿" INVERTED QUESTION MARK
- /* 48 */ "!fixedColumnOrder!9,\",\',#,-,\u00A1,!,\u00BF,\\,,?,@,&,\\%,+,;,:,/,(,)",
- /* 49~ */
+ /* 49 */ "!fixedColumnOrder!9,\u00A1,\",\',#,-,:,!,\\,,?,\u00BF,@,&,\\%,+,;,/,(,)",
+ /* 50~ */
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, null, null, null, null, null, null, null, null, null,
- /* ~89 */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null,
+ /* ~99 */
// U+00A1: "¡" INVERTED EXCLAMATION MARK
- /* 90 */ "\u00A1",
+ /* 100 */ "!,\u00A1",
+ /* 101 */ null,
// U+00BF: "¿" INVERTED QUESTION MARK
- /* 91 */ "\u00BF",
- /* 92 */ null,
- /* 93 */ null,
- /* 94 */ "!",
- /* 95 */ "?",
- /* 96~ */
- null, null, null,
- /* ~98 */
- /* 99 */ "\u00A1",
- /* 100 */ "\u00A1,!",
- /* 101 */ "\u00BF",
- /* 102 */ "\u00BF,?",
+ /* 102 */ "?,\u00BF",
};
/* Language et: Estonian */
@@ -993,33 +1192,33 @@ 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,
+ /* ~42 */
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
// <string name="more_keys_for_double_quote">&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB;|&#x00AB;</string>
- /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\",\'",
+ /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\",\'",
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
// <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB|&#x00AB;;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
- /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
- /* 44~ */
+ /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
+ /* 45~ */
null, null, null, null,
- /* ~47 */
+ /* ~48 */
// U+061F: "؟" ARABIC QUESTION MARK
// U+060C: "،" ARABIC COMMA
// U+061B: "؛" ARABIC SEMICOLON
- /* 48 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
+ /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)",
// U+2605: "★" BLACK STAR
// U+066D: "٭" ARABIC FIVE POINTED STAR
- /* 49 */ "\u2605,\u066D",
+ /* 50 */ "\u2605,\u066D",
// U+266A: "♪" EIGHTH NOTE
- /* 50 */ "\u266A",
- /* 51 */ null,
+ /* 51 */ "\u266A",
+ /* 52 */ 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
- /* 52 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
- /* 53 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[",
+ /* 53 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]",
+ /* 54 */ "!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
@@ -1035,8 +1234,8 @@ public final class KeyboardTextsSet {
// U+201D: "”" RIGHT DOUBLE QUOTATION MARK
// U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
// U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
- /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>",
- /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<",
+ /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>",
+ /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<",
// U+0655: "ٕ" ARABIC HAMZA BELOW
// U+0652: "ْ" ARABIC SUKUN
// U+0651: "ّ" ARABIC SHADDA
@@ -1052,47 +1251,46 @@ public final class KeyboardTextsSet {
// U+064E: "َ" ARABIC FATHA
// U+0640: "ـ" ARABIC TATWEEL
// In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label.
- /* 56 */ "!fixedColumnOrder!7,\u0655,\u0652,\u0651,\u064C,\u064D,\u064B,\u0654,\u0656,\u0670,\u0653,\u064F,\u0650,\u064E,\u0640\u0640\u0640|\u0640",
- /* 57 */ "\u064B",
+ /* 57 */ "!fixedColumnOrder!7,\u0655,\u0652,\u0651,\u064C,\u064D,\u064B,\u0654,\u0656,\u0670,\u0653,\u064F,\u0650,\u064E,\u0640\u0640\u0640|\u0640",
+ /* 58 */ "\u064B",
// U+06F1: "۱" EXTENDED ARABIC-INDIC DIGIT ONE
- /* 58 */ "\u06F1",
+ /* 59 */ "\u06F1",
// U+06F2: "۲" EXTENDED ARABIC-INDIC DIGIT TWO
- /* 59 */ "\u06F2",
+ /* 60 */ "\u06F2",
// U+06F3: "۳" EXTENDED ARABIC-INDIC DIGIT THREE
- /* 60 */ "\u06F3",
+ /* 61 */ "\u06F3",
// U+06F4: "۴" EXTENDED ARABIC-INDIC DIGIT FOUR
- /* 61 */ "\u06F4",
+ /* 62 */ "\u06F4",
// U+06F5: "۵" EXTENDED ARABIC-INDIC DIGIT FIVE
- /* 62 */ "\u06F5",
+ /* 63 */ "\u06F5",
// U+06F6: "۶" EXTENDED ARABIC-INDIC DIGIT SIX
- /* 63 */ "\u06F6",
+ /* 64 */ "\u06F6",
// U+06F7: "۷" EXTENDED ARABIC-INDIC DIGIT SEVEN
- /* 64 */ "\u06F7",
+ /* 65 */ "\u06F7",
// U+06F8: "۸" EXTENDED ARABIC-INDIC DIGIT EIGHT
- /* 65 */ "\u06F8",
+ /* 66 */ "\u06F8",
// U+06F9: "۹" EXTENDED ARABIC-INDIC DIGIT NINE
- /* 66 */ "\u06F9",
+ /* 67 */ "\u06F9",
// U+06F0: "۰" EXTENDED ARABIC-INDIC DIGIT ZERO
- /* 67 */ "\u06F0",
- /* 68 */ "1",
- /* 69 */ "2",
- /* 70 */ "3",
- /* 71 */ "4",
- /* 72 */ "5",
- /* 73 */ "6",
- /* 74 */ "7",
- /* 75 */ "8",
- /* 76 */ "9",
+ /* 68 */ "\u06F0",
+ /* 69 */ "1",
+ /* 70 */ "2",
+ /* 71 */ "3",
+ /* 72 */ "4",
+ /* 73 */ "5",
+ /* 74 */ "6",
+ /* 75 */ "7",
+ /* 76 */ "8",
+ /* 77 */ "9",
// U+066B: "٫" ARABIC DECIMAL SEPARATOR
// U+066C: "٬" ARABIC THOUSANDS SEPARATOR
- /* 77 */ "0,\u066B,\u066C",
- /* 78~ */
+ /* 78 */ "0,\u066B,\u066C",
+ /* 79~ */
null, null, null, null, null, null, null, null, null, null,
- /* ~87 */
+ /* ~88 */
// U+060C: "،" ARABIC COMMA
- /* 88 */ "\u060C",
- /* 89 */ "\\,",
- /* 90 */ null,
+ /* 89 */ "\u060C",
+ /* 90 */ "\\,",
/* 91 */ "\u061F",
/* 92 */ "\u061B",
// U+066A: "٪" ARABIC PERCENT SIGN
@@ -1223,38 +1421,38 @@ 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,
- /* ~57 */
+ null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+ /* ~58 */
// U+0967: "१" DEVANAGARI DIGIT ONE
- /* 58 */ "\u0967",
+ /* 59 */ "\u0967",
// U+0968: "२" DEVANAGARI DIGIT TWO
- /* 59 */ "\u0968",
+ /* 60 */ "\u0968",
// U+0969: "३" DEVANAGARI DIGIT THREE
- /* 60 */ "\u0969",
+ /* 61 */ "\u0969",
// U+096A: "४" DEVANAGARI DIGIT FOUR
- /* 61 */ "\u096A",
+ /* 62 */ "\u096A",
// U+096B: "५" DEVANAGARI DIGIT FIVE
- /* 62 */ "\u096B",
+ /* 63 */ "\u096B",
// U+096C: "६" DEVANAGARI DIGIT SIX
- /* 63 */ "\u096C",
+ /* 64 */ "\u096C",
// U+096D: "७" DEVANAGARI DIGIT SEVEN
- /* 64 */ "\u096D",
+ /* 65 */ "\u096D",
// U+096E: "८" DEVANAGARI DIGIT EIGHT
- /* 65 */ "\u096E",
+ /* 66 */ "\u096E",
// U+096F: "९" DEVANAGARI DIGIT NINE
- /* 66 */ "\u096F",
+ /* 67 */ "\u096F",
// U+0966: "०" DEVANAGARI DIGIT ZERO
- /* 67 */ "\u0966",
- /* 68 */ "1",
- /* 69 */ "2",
- /* 70 */ "3",
- /* 71 */ "4",
- /* 72 */ "5",
- /* 73 */ "6",
- /* 74 */ "7",
- /* 75 */ "8",
- /* 76 */ "9",
- /* 77 */ "0",
+ /* 68 */ "\u0966",
+ /* 69 */ "1",
+ /* 70 */ "2",
+ /* 71 */ "3",
+ /* 72 */ "4",
+ /* 73 */ "5",
+ /* 74 */ "6",
+ /* 75 */ "7",
+ /* 76 */ "8",
+ /* 77 */ "9",
+ /* 78 */ "0",
};
/* Language hr: Croatian */
@@ -1442,27 +1640,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, null, null, null, null, null, null,
- /* ~41 */
+ null, null, null, null, null, null, null, null, null, null, null, null, null,
+ /* ~42 */
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
// <string name="more_keys_for_double_quote">&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB;|&#x00AB;</string>
- /* 42 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB",
+ /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB",
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK
// <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;|&#x00BB;,&#x00BB|&#x00AB;;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
- /* 43 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
- /* 44~ */
+ /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B",
+ /* 45~ */
null, null, null, null, null,
- /* ~48 */
+ /* ~49 */
// U+2605: "★" BLACK STAR
- /* 49 */ "\u2605",
- /* 50 */ null,
+ /* 50 */ "\u2605",
+ /* 51 */ null,
// U+00B1: "±" PLUS-MINUS SIGN
// U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN
- /* 51 */ "\u00B1,\uFB29",
+ /* 52 */ "\u00B1,\uFB29",
// The all letters need to be mirrored are found at
// http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt
- /* 52 */ "!fixedColumnOrder!3,<|>,{|},[|]",
- /* 53 */ "!fixedColumnOrder!3,>|<,}|{,]|[",
+ /* 53 */ "!fixedColumnOrder!3,<|>,{|},[|]",
+ /* 54 */ "!fixedColumnOrder!3,>|<,}|{,]|[",
// U+2264: "≤" LESS-THAN OR EQUAL TO
// U+2265: "≥" GREATER-THAN EQUAL TO
// U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
@@ -1478,8 +1676,8 @@ public final class KeyboardTextsSet {
// U+201D: "”" RIGHT DOUBLE QUOTATION MARK
// U+201E: "„" DOUBLE LOW-9 QUOTATION MARK
// U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
- /* 54 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
- /* 55 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
+ /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB",
+ /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB",
};
/* Language ky: Kirghiz */
@@ -1490,22 +1688,29 @@ public final class KeyboardTextsSet {
/* ~24 */
// U+0449: "щ" CYRILLIC SMALL LETTER SHCHA
/* 25 */ "\u0449",
+ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+ /* 26 */ "\u044A",
// U+044B: "ы" CYRILLIC SMALL LETTER YERU
- /* 26 */ "\u044B",
+ /* 27 */ "\u044B",
+ // U+044D: "э" CYRILLIC SMALL LETTER E
+ /* 28 */ "\u044D",
// U+0438: "и" CYRILLIC SMALL LETTER I
- /* 27 */ "\u0438",
+ /* 29 */ "\u0438",
// U+04AF: "ү" CYRILLIC SMALL LETTER STRAIGHT U
- /* 28 */ "\u04AF",
- /* 29 */ null,
+ /* 30 */ "\u04AF",
// U+04A3: "ң" CYRILLIC SMALL LETTER EN WITH DESCENDER
- /* 30 */ "\u04A3",
- // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 31 */ "\u044A",
+ /* 31 */ "\u04A3",
/* 32 */ null,
+ /* 33 */ null,
// U+04E9: "ө" CYRILLIC SMALL LETTER BARRED O
- /* 33 */ "\u04E9",
+ /* 34 */ "\u04E9",
// U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 34 */ "\u044A",
+ /* 35 */ "\u044A",
+ /* 36~ */
+ null, null, null, null,
+ /* ~39 */
+ // U+0451: "ё" CYRILLIC SMALL LETTER IO
+ /* 40 */ "\u0451",
};
/* Language lt: Lithuanian */
@@ -1692,21 +1897,21 @@ 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,
- /* ~34 */
+ null, null, null, null, null, null,
+ /* ~35 */
// U+0455: "ѕ" CYRILLIC SMALL LETTER DZE
- /* 35 */ "\u0455",
+ /* 36 */ "\u0455",
// U+045C: "ќ" CYRILLIC SMALL LETTER KJE
- /* 36 */ "\u045C",
+ /* 37 */ "\u045C",
// U+0437: "з" CYRILLIC SMALL LETTER ZE
- /* 37 */ "\u0437",
+ /* 38 */ "\u0437",
// U+0453: "ѓ" CYRILLIC SMALL LETTER GJE
- /* 38 */ "\u0453",
+ /* 39 */ "\u0453",
// U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE
- /* 39 */ "\u0450",
+ /* 40 */ "\u0450",
// U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE
- /* 40 */ "\u045D",
- /* 41 */ null,
+ /* 41 */ "\u045D",
+ /* 42 */ null,
// U+2018: "‘" LEFT SINGLE QUOTATION MARK
// U+2019: "’" RIGHT SINGLE QUOTATION MARK
// U+201A: "‚" SINGLE LOW-9 QUOTATION MARK
@@ -1717,10 +1922,10 @@ public final class KeyboardTextsSet {
// U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
// <string name="more_keys_for_double_quote">!fixedColumnOrder!6,&#x201E;,&#x201C;,&#x201D;,&#x201F;,&#x00AB;,&#x00BB;</string>
- /* 42 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB",
+ /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB",
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
// <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
- /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
+ /* 44 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
};
/* Language nb: Norwegian Bokmål */
@@ -1982,20 +2187,24 @@ public final class KeyboardTextsSet {
/* ~24 */
// U+0449: "щ" CYRILLIC SMALL LETTER SHCHA
/* 25 */ "\u0449",
+ // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
+ /* 26 */ "\u044A",
// U+044B: "ы" CYRILLIC SMALL LETTER YERU
- /* 26 */ "\u044B",
+ /* 27 */ "\u044B",
+ // U+044D: "э" CYRILLIC SMALL LETTER E
+ /* 28 */ "\u044D",
// U+0438: "и" CYRILLIC SMALL LETTER I
- /* 27 */ "\u0438",
- /* 28 */ null,
- // U+0451: "ё" CYRILLIC SMALL LETTER IO
- /* 29 */ "\u0451",
- /* 30 */ null,
- // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 31 */ "\u044A",
- /* 32 */ null,
- /* 33 */ null,
+ /* 29 */ "\u0438",
+ /* 30~ */
+ null, null, null, null, null,
+ /* ~34 */
// U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 34 */ "\u044A",
+ /* 35 */ "\u044A",
+ /* 36~ */
+ null, null, null, null,
+ /* ~39 */
+ // U+0451: "ё" CYRILLIC SMALL LETTER IO
+ /* 40 */ "\u0451",
};
/* Language sk: Slovak */
@@ -2113,21 +2322,40 @@ 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,
- /* ~34 */
+ null, null, null, null, null, null,
+ /* ~35 */
+ // TODO: Move these to sr-Latn once we can handle IETF language tag with script name specified.
+ // BEGIN: More keys definitions for Serbian (Latin)
+ // U+0161: "š" LATIN SMALL LETTER S WITH CARON
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ // U+015B: "ś" LATIN SMALL LETTER S WITH ACUTE
+ // <string name="more_keys_for_s">&#x0161;,&#x00DF;,&#x015B;</string>
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ // <string name="more_keys_for_c">&#x010D;,&#x00E7;,&#x0107;</string>
+ // U+010F: "ď" LATIN SMALL LETTER D WITH CARON
+ // <string name="more_keys_for_d">&#x010F;</string>
+ // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON
+ // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE
+ // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE
+ // <string name="more_keys_for_z">&#x017E;,&#x017A;,&#x017C;</string>
+ // END: More keys definitions for Serbian (Latin)
+ // BEGIN: More keys definitions for Serbian (Cyrillic)
// U+0437: "з" CYRILLIC SMALL LETTER ZE
- /* 35 */ "\u0437",
+ /* 36 */ "\u0437",
// U+045B: "ћ" CYRILLIC SMALL LETTER TSHE
- /* 36 */ "\u045B",
+ /* 37 */ "\u045B",
// U+0455: "ѕ" CYRILLIC SMALL LETTER DZE
- /* 37 */ "\u0455",
+ /* 38 */ "\u0455",
// U+0452: "ђ" CYRILLIC SMALL LETTER DJE
- /* 38 */ "\u0452",
+ /* 39 */ "\u0452",
// U+0450: "ѐ" CYRILLIC SMALL LETTER IE WITH GRAVE
- /* 39 */ "\u0450",
+ /* 40 */ "\u0450",
// U+045D: "ѝ" CYRILLIC SMALL LETTER I WITH GRAVE
- /* 40 */ "\u045D",
- /* 41 */ null,
+ /* 41 */ "\u045D",
+ /* 42 */ null,
+ // END: More keys definitions for Serbian (Cyrillic)
// U+2018: "‘" LEFT SINGLE QUOTATION MARK
// U+2019: "’" RIGHT SINGLE QUOTATION MARK
// U+201A: "‚" SINGLE LOW-9 QUOTATION MARK
@@ -2138,10 +2366,10 @@ public final class KeyboardTextsSet {
// U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
// <string name="more_keys_for_double_quote">!fixedColumnOrder!6,&#x201E;,&#x201C;,&#x201D;,&#x201F;,&#x00AB;,&#x00BB;</string>
- /* 42 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB",
+ /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB",
// TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK.
// <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,&#x201C;,&#x201D;,&#x201E;,&#x201F;,&#x00AB;,&#x00BB;,&#x2018;,&#x2019;,&#x201A;,&#x201B;</string>
- /* 43 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
+ /* 44 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B",
};
/* Language sv: Swedish */
@@ -2186,6 +2414,111 @@ public final class KeyboardTextsSet {
/* 24 */ "\u00E6",
};
+ /* Language sw: Swahili */
+ private static final String[] LANGUAGE_sw = {
+ // This is the same as English except more_keys_for_g.
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101",
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113",
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC",
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5",
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B",
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ /* 5 */ "\u00DF",
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ /* 6 */ "\u00F1",
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ /* 7 */ "\u00E7",
+ /* 8~ */
+ null, null, null, null, null, null, null,
+ /* ~14 */
+ /* 15 */ "g\'",
+ };
+
+ /* Language tl: Tagalog */
+ private static final String[] LANGUAGE_tl = {
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0105: "ą" LATIN SMALL LETTER A WITH OGONEK
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ // U+00AA: "ª" FEMININE ORDINAL INDICATOR
+ /* 0 */ "\u00E1,\u00E0,\u00E4,\u00E2,\u00E3,\u00E5,\u0105,\u00E6,\u0101,\u00AA",
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+0119: "ę" LATIN SMALL LETTER E WITH OGONEK
+ // U+0117: "ė" LATIN SMALL LETTER E WITH DOT ABOVE
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E9,\u00E8,\u00EB,\u00EA,\u0119,\u0117,\u0113",
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+012F: "į" LATIN SMALL LETTER I WITH OGONEK
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ /* 2 */ "\u00ED,\u00EF,\u00EC,\u00EE,\u012F,\u012B",
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ // U+00BA: "º" MASCULINE ORDINAL INDICATOR
+ /* 3 */ "\u00F3,\u00F2,\u00F6,\u00F4,\u00F5,\u00F8,\u0153,\u014D,\u00BA",
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FA,\u00FC,\u00F9,\u00FB,\u016B",
+ /* 5 */ null,
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE
+ /* 6 */ "\u00F1,\u0144",
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE
+ // U+010D: "č" LATIN SMALL LETTER C WITH CARON
+ /* 7 */ "\u00E7,\u0107,\u010D",
+ };
+
/* Language tr: Turkish */
private static final String[] LANGUAGE_tr = {
// U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
@@ -2239,20 +2572,23 @@ public final class KeyboardTextsSet {
/* ~24 */
// U+0449: "щ" CYRILLIC SMALL LETTER SHCHA
/* 25 */ "\u0449",
+ // U+0457: "ї" CYRILLIC SMALL LETTER YI
+ /* 26 */ "\u0457",
// U+0456: "і" CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I
- /* 26 */ "\u0456",
+ /* 27 */ "\u0456",
+ // U+0454: "є" CYRILLIC SMALL LETTER UKRAINIAN IE
+ /* 28 */ "\u0454",
// U+0438: "и" CYRILLIC SMALL LETTER I
- /* 27 */ "\u0438",
- /* 28~ */
- null, null, null,
- /* ~30 */
- // U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 31 */ "\u044A",
+ /* 29 */ "\u0438",
+ /* 30 */ null,
+ /* 31 */ null,
+ // U+0491: "ґ" CYRILLIC SMALL LETTER GHE WITH UPTURN
+ /* 32 */ "\u0491",
// U+0457: "ї" CYRILLIC SMALL LETTER YI
- /* 32 */ "\u0457",
- /* 33 */ null,
+ /* 33 */ "\u0457",
+ /* 34 */ null,
// U+044A: "ъ" CYRILLIC SMALL LETTER HARD SIGN
- /* 34 */ "\u044A",
+ /* 35 */ "\u044A",
};
/* Language vi: Vietnamese */
@@ -2336,6 +2672,53 @@ public final class KeyboardTextsSet {
/* 9 */ "\u0111",
};
+ /* Language zu: Zulu */
+ private static final String[] LANGUAGE_zu = {
+ // This is the same as English
+ // U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
+ // U+00E1: "á" LATIN SMALL LETTER A WITH ACUTE
+ // U+00E2: "â" LATIN SMALL LETTER A WITH CIRCUMFLEX
+ // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS
+ // U+00E6: "æ" LATIN SMALL LETTER AE
+ // U+00E3: "ã" LATIN SMALL LETTER A WITH TILDE
+ // U+00E5: "å" LATIN SMALL LETTER A WITH RING ABOVE
+ // U+0101: "ā" LATIN SMALL LETTER A WITH MACRON
+ /* 0 */ "\u00E0,\u00E1,\u00E2,\u00E4,\u00E6,\u00E3,\u00E5,\u0101",
+ // U+00E8: "è" LATIN SMALL LETTER E WITH GRAVE
+ // U+00E9: "é" LATIN SMALL LETTER E WITH ACUTE
+ // U+00EA: "ê" LATIN SMALL LETTER E WITH CIRCUMFLEX
+ // U+00EB: "ë" LATIN SMALL LETTER E WITH DIAERESIS
+ // U+0113: "ē" LATIN SMALL LETTER E WITH MACRON
+ /* 1 */ "\u00E8,\u00E9,\u00EA,\u00EB,\u0113",
+ // U+00EE: "î" LATIN SMALL LETTER I WITH CIRCUMFLEX
+ // U+00EF: "ï" LATIN SMALL LETTER I WITH DIAERESIS
+ // U+00ED: "í" LATIN SMALL LETTER I WITH ACUTE
+ // U+012B: "ī" LATIN SMALL LETTER I WITH MACRON
+ // U+00EC: "ì" LATIN SMALL LETTER I WITH GRAVE
+ /* 2 */ "\u00EE,\u00EF,\u00ED,\u012B,\u00EC",
+ // U+00F4: "ô" LATIN SMALL LETTER O WITH CIRCUMFLEX
+ // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS
+ // U+00F2: "ò" LATIN SMALL LETTER O WITH GRAVE
+ // U+00F3: "ó" LATIN SMALL LETTER O WITH ACUTE
+ // U+0153: "œ" LATIN SMALL LIGATURE OE
+ // U+00F8: "ø" LATIN SMALL LETTER O WITH STROKE
+ // U+014D: "ō" LATIN SMALL LETTER O WITH MACRON
+ // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE
+ /* 3 */ "\u00F4,\u00F6,\u00F2,\u00F3,\u0153,\u00F8,\u014D,\u00F5",
+ // U+00FB: "û" LATIN SMALL LETTER U WITH CIRCUMFLEX
+ // U+00FC: "ü" LATIN SMALL LETTER U WITH DIAERESIS
+ // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE
+ // U+00FA: "ú" LATIN SMALL LETTER U WITH ACUTE
+ // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON
+ /* 4 */ "\u00FB,\u00FC,\u00F9,\u00FA,\u016B",
+ // U+00DF: "ß" LATIN SMALL LETTER SHARP S
+ /* 5 */ "\u00DF",
+ // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE
+ /* 6 */ "\u00F1",
+ // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA
+ /* 7 */ "\u00E7",
+ };
+
/* Language zz: No language */
private static final String[] LANGUAGE_zz = {
// U+00E0: "à" LATIN SMALL LETTER A WITH GRAVE
@@ -2461,6 +2844,7 @@ public final class KeyboardTextsSet {
private static final Object[] LANGUAGES_AND_TEXTS = {
"DEFAULT", LANGUAGE_DEFAULT, /* default */
+ "af", LANGUAGE_af, /* Afrikaans */
"ar", LANGUAGE_ar, /* Arabic */
"be", LANGUAGE_be, /* Belarusian */
"ca", LANGUAGE_ca, /* Catalan */
@@ -2468,6 +2852,7 @@ public final class KeyboardTextsSet {
"da", LANGUAGE_da, /* Danish */
"de", LANGUAGE_de, /* German */
"en", LANGUAGE_en, /* English */
+ "eo", LANGUAGE_eo, /* Esperanto */
"es", LANGUAGE_es, /* Spanish */
"et", LANGUAGE_et, /* Estonian */
"fa", LANGUAGE_fa, /* Persian */
@@ -2494,9 +2879,12 @@ public final class KeyboardTextsSet {
"sl", LANGUAGE_sl, /* Slovenian */
"sr", LANGUAGE_sr, /* Serbian */
"sv", LANGUAGE_sv, /* Swedish */
+ "sw", LANGUAGE_sw, /* Swahili */
+ "tl", LANGUAGE_tl, /* Tagalog */
"tr", LANGUAGE_tr, /* Turkish */
"uk", LANGUAGE_uk, /* Ukrainian */
"vi", LANGUAGE_vi, /* Vietnamese */
+ "zu", LANGUAGE_zu, /* Zulu */
"zz", LANGUAGE_zz, /* No language */
};
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java
new file mode 100644
index 000000000..d1b4c8524
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeysCache.java
@@ -0,0 +1,40 @@
+/*
+ * 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.keyboard.internal;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.latin.CollectionUtils;
+
+import java.util.HashMap;
+
+public final class KeysCache {
+ private final HashMap<Key, Key> mMap = CollectionUtils.newHashMap();
+
+ public void clear() {
+ mMap.clear();
+ }
+
+ public Key get(final Key key) {
+ final Key existingKey = mMap.get(key);
+ if (existingKey != null) {
+ // Reuse the existing element that equals to "key" without adding "key" to the map.
+ return existingKey;
+ }
+ mMap.put(key, key);
+ return key;
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
new file mode 100644
index 000000000..550391b49
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
@@ -0,0 +1,86 @@
+/*
+ * 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.keyboard.internal;
+
+import android.text.TextUtils;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.StringUtils;
+
+import java.util.Locale;
+
+public final class MoreKeySpec {
+ public final int mCode;
+ public final String mLabel;
+ public final String mOutputText;
+ public final int mIconId;
+
+ public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale,
+ final KeyboardCodesSet codesSet) {
+ mLabel = KeySpecParser.toUpperCaseOfStringForLocale(
+ KeySpecParser.getLabel(moreKeySpec), needsToUpperCase, locale);
+ final int code = KeySpecParser.toUpperCaseOfCodeForLocale(
+ KeySpecParser.getCode(moreKeySpec, codesSet), needsToUpperCase, locale);
+ if (code == Keyboard.CODE_UNSPECIFIED) {
+ // Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters
+ // upper case representation ("SS").
+ mCode = Keyboard.CODE_OUTPUT_TEXT;
+ mOutputText = mLabel;
+ } else {
+ mCode = code;
+ mOutputText = KeySpecParser.toUpperCaseOfStringForLocale(
+ KeySpecParser.getOutputText(moreKeySpec), needsToUpperCase, locale);
+ }
+ mIconId = KeySpecParser.getIconId(moreKeySpec);
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 1;
+ hashCode = 31 + mCode;
+ hashCode = hashCode * 31 + mIconId;
+ hashCode = hashCode * 31 + (mLabel == null ? 0 : mLabel.hashCode());
+ hashCode = hashCode * 31 + (mOutputText == null ? 0 : mOutputText.hashCode());
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o instanceof MoreKeySpec) {
+ final MoreKeySpec other = (MoreKeySpec)o;
+ return mCode == other.mCode
+ && mIconId == other.mIconId
+ && TextUtils.equals(mLabel, other.mLabel)
+ && TextUtils.equals(mOutputText, other.mOutputText);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel
+ : KeySpecParser.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
+ final String output = (mCode == Keyboard.CODE_OUTPUT_TEXT ? mOutputText
+ : Keyboard.printableCode(mCode));
+ if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) {
+ return output;
+ } else {
+ return label + "|" + output;
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
index 5db65c660..a52f202aa 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
@@ -18,72 +18,164 @@ package com.android.inputmethod.keyboard.internal;
import android.util.Log;
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.latin.CollectionUtils;
-import java.util.Iterator;
-import java.util.LinkedList;
+import java.util.ArrayList;
-public class PointerTrackerQueue {
+public final class PointerTrackerQueue {
private static final String TAG = PointerTrackerQueue.class.getSimpleName();
private static final boolean DEBUG = false;
- private final LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>();
+ public interface Element {
+ public boolean isModifier();
+ public boolean isInSlidingKeyInput();
+ public void onPhantomUpEvent(long eventTime);
+ }
+
+ private static final int INITIAL_CAPACITY = 10;
+ private final ArrayList<Element> mExpandableArrayOfActivePointers =
+ CollectionUtils.newArrayList(INITIAL_CAPACITY);
+ private int mArraySize = 0;
- public synchronized void add(PointerTracker tracker) {
- mQueue.add(tracker);
+ public synchronized int size() {
+ return mArraySize;
}
- public synchronized void remove(PointerTracker tracker) {
- mQueue.remove(tracker);
+ public synchronized void add(final Element pointer) {
+ final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+ final int arraySize = mArraySize;
+ if (arraySize < expandableArray.size()) {
+ expandableArray.set(arraySize, pointer);
+ } else {
+ expandableArray.add(pointer);
+ }
+ mArraySize = arraySize + 1;
}
- public synchronized void releaseAllPointersOlderThan(PointerTracker tracker,
- long eventTime) {
+ public synchronized void remove(final Element pointer) {
+ final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+ final int arraySize = mArraySize;
+ int newSize = 0;
+ for (int index = 0; index < arraySize; index++) {
+ final Element element = expandableArray.get(index);
+ if (element == pointer) {
+ if (newSize != index) {
+ Log.w(TAG, "Found duplicated element in remove: " + pointer);
+ }
+ continue; // Remove this element from the expandableArray.
+ }
+ if (newSize != index) {
+ // Shift this element toward the beginning of the expandableArray.
+ expandableArray.set(newSize, element);
+ }
+ newSize++;
+ }
+ mArraySize = newSize;
+ }
+
+ public synchronized Element getOldestElement() {
+ return (mArraySize == 0) ? null : mExpandableArrayOfActivePointers.get(0);
+ }
+
+ public synchronized void releaseAllPointersOlderThan(final Element pointer,
+ final long eventTime) {
if (DEBUG) {
- Log.d(TAG, "releaseAllPoniterOlderThan: [" + tracker.mPointerId + "] " + this);
+ Log.d(TAG, "releaseAllPoniterOlderThan: " + pointer + " " + this);
}
- if (!mQueue.contains(tracker)) {
- return;
+ final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+ final int arraySize = mArraySize;
+ int newSize, index;
+ for (newSize = index = 0; index < arraySize; index++) {
+ final Element element = expandableArray.get(index);
+ if (element == pointer) {
+ break; // Stop releasing elements.
+ }
+ if (!element.isModifier()) {
+ element.onPhantomUpEvent(eventTime);
+ continue; // Remove this element from the expandableArray.
+ }
+ if (newSize != index) {
+ // Shift this element toward the beginning of the expandableArray.
+ expandableArray.set(newSize, element);
+ }
+ newSize++;
}
- final Iterator<PointerTracker> it = mQueue.iterator();
- while (it.hasNext()) {
- final PointerTracker t = it.next();
- if (t == tracker) {
- break;
+ // Shift rest of the expandableArray.
+ int count = 0;
+ for (; index < arraySize; index++) {
+ final Element element = expandableArray.get(index);
+ if (element == pointer) {
+ if (count > 0) {
+ Log.w(TAG, "Found duplicated element in releaseAllPointersOlderThan: "
+ + pointer);
+ }
+ count++;
}
- if (!t.isModifier()) {
- t.onPhantomUpEvent(t.getLastX(), t.getLastY(), eventTime);
- it.remove();
+ if (newSize != index) {
+ expandableArray.set(newSize, expandableArray.get(index));
+ newSize++;
}
}
+ mArraySize = newSize;
}
- public void releaseAllPointers(long eventTime) {
+ public void releaseAllPointers(final long eventTime) {
releaseAllPointersExcept(null, eventTime);
}
- public synchronized void releaseAllPointersExcept(PointerTracker tracker, long eventTime) {
+ public synchronized void releaseAllPointersExcept(final Element pointer,
+ final long eventTime) {
if (DEBUG) {
- if (tracker == null) {
+ if (pointer == null) {
Log.d(TAG, "releaseAllPoniters: " + this);
} else {
- Log.d(TAG, "releaseAllPoniterExcept: [" + tracker.mPointerId + "] " + this);
+ Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this);
}
}
- final Iterator<PointerTracker> it = mQueue.iterator();
- while (it.hasNext()) {
- final PointerTracker t = it.next();
- if (t != tracker) {
- t.onPhantomUpEvent(t.getLastX(), t.getLastY(), eventTime);
- it.remove();
+ final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+ final int arraySize = mArraySize;
+ int newSize = 0, count = 0;
+ for (int index = 0; index < arraySize; index++) {
+ final Element element = expandableArray.get(index);
+ if (element == pointer) {
+ if (count > 0) {
+ Log.w(TAG, "Found duplicated element in releaseAllPointersExcept: " + pointer);
+ }
+ count++;
+ } else {
+ element.onPhantomUpEvent(eventTime);
+ continue; // Remove this element from the expandableArray.
}
+ if (newSize != index) {
+ // Shift this element toward the beginning of the expandableArray.
+ expandableArray.set(newSize, element);
+ }
+ newSize++;
}
+ mArraySize = newSize;
+ }
+
+ public synchronized boolean hasModifierKeyOlderThan(final Element pointer) {
+ final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+ final int arraySize = mArraySize;
+ for (int index = 0; index < arraySize; index++) {
+ final Element element = expandableArray.get(index);
+ if (element == pointer) {
+ return false; // Stop searching modifier key.
+ }
+ if (element.isModifier()) {
+ return true;
+ }
+ }
+ return false;
}
public synchronized boolean isAnyInSlidingKeyInput() {
- for (final PointerTracker tracker : mQueue) {
- if (tracker.isInSlidingKeyInput()) {
+ final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+ final int arraySize = mArraySize;
+ for (int index = 0; index < arraySize; index++) {
+ final Element element = expandableArray.get(index);
+ if (element.isInSlidingKeyInput()) {
return true;
}
}
@@ -91,14 +183,16 @@ public class PointerTrackerQueue {
}
@Override
- public String toString() {
+ public synchronized String toString() {
final StringBuilder sb = new StringBuilder();
- for (final PointerTracker tracker : mQueue) {
+ final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+ final int arraySize = mArraySize;
+ for (int index = 0; index < arraySize; index++) {
+ final Element element = expandableArray.get(index);
if (sb.length() > 0)
sb.append(" ");
- sb.append("[" + tracker.mPointerId + " "
- + Keyboard.printableCode(tracker.getKey().mCode) + "]");
+ sb.append(element.toString());
}
- return sb.toString();
+ return "[" + sb.toString() + "]";
}
}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
new file mode 100644
index 000000000..776ac0204
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/PreviewPlacerView.java
@@ -0,0 +1,348 @@
+/*
+ * 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.keyboard.internal;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.widget.RelativeLayout;
+
+import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.keyboard.internal.GesturePreviewTrail.Params;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+
+public final class PreviewPlacerView extends RelativeLayout {
+ // The height of extra area above the keyboard to draw gesture trails.
+ // Proportional to the keyboard height.
+ private static final float EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO = 0.25f;
+
+ private final int mGestureFloatingPreviewTextColor;
+ private final int mGestureFloatingPreviewTextOffset;
+ private final int mGestureFloatingPreviewColor;
+ private final float mGestureFloatingPreviewHorizontalPadding;
+ private final float mGestureFloatingPreviewVerticalPadding;
+ private final float mGestureFloatingPreviewRoundRadius;
+
+ private int mKeyboardViewOriginX;
+ private int mKeyboardViewOriginY;
+
+ private final SparseArray<GesturePreviewTrail> mGesturePreviewTrails =
+ CollectionUtils.newSparseArray();
+ private final Params mGesturePreviewTrailParams;
+ private final Paint mGesturePaint;
+ private boolean mDrawsGesturePreviewTrail;
+ private int mOffscreenWidth;
+ private int mOffscreenHeight;
+ private int mOffscreenOffsetY;
+ private Bitmap mOffscreenBuffer;
+ private final Canvas mOffscreenCanvas = new Canvas();
+ private final Rect mOffscreenDirtyRect = new Rect();
+ private final Rect mGesturePreviewTrailBoundsRect = new Rect(); // per trail
+
+ private final Paint mTextPaint;
+ private String mGestureFloatingPreviewText;
+ private final int mGestureFloatingPreviewTextHeight;
+ // {@link RectF} is needed for {@link Canvas#drawRoundRect(RectF, float, float, Paint)}.
+ private final RectF mGestureFloatingPreviewRectangle = new RectF();
+ private int mLastPointerX;
+ private int mLastPointerY;
+ private static final char[] TEXT_HEIGHT_REFERENCE_CHAR = { 'M' };
+ private boolean mDrawsGestureFloatingPreviewText;
+
+ private final DrawingHandler mDrawingHandler;
+
+ private static final class DrawingHandler extends StaticInnerHandlerWrapper<PreviewPlacerView> {
+ private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 0;
+ private static final int MSG_UPDATE_GESTURE_PREVIEW_TRAIL = 1;
+
+ private final Params mGesturePreviewTrailParams;
+ private final int mGestureFloatingPreviewTextLingerTimeout;
+
+ public DrawingHandler(final PreviewPlacerView outerInstance,
+ final Params gesturePreviewTrailParams,
+ final int getstureFloatinPreviewTextLinerTimeout) {
+ super(outerInstance);
+ mGesturePreviewTrailParams = gesturePreviewTrailParams;
+ mGestureFloatingPreviewTextLingerTimeout = getstureFloatinPreviewTextLinerTimeout;
+ }
+
+ @Override
+ public void handleMessage(final Message msg) {
+ final PreviewPlacerView placerView = getOuterInstance();
+ if (placerView == null) return;
+ switch (msg.what) {
+ case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT:
+ placerView.setGestureFloatingPreviewText(null);
+ break;
+ case MSG_UPDATE_GESTURE_PREVIEW_TRAIL:
+ placerView.invalidate();
+ break;
+ }
+ }
+
+ public void dismissGestureFloatingPreviewText() {
+ removeMessages(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
+ sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT),
+ mGestureFloatingPreviewTextLingerTimeout);
+ }
+
+ public void postUpdateGestureTrailPreview() {
+ removeMessages(MSG_UPDATE_GESTURE_PREVIEW_TRAIL);
+ sendMessageDelayed(obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_TRAIL),
+ mGesturePreviewTrailParams.mUpdateInterval);
+ }
+ }
+
+ public PreviewPlacerView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, R.attr.keyboardViewStyle);
+ }
+
+ public PreviewPlacerView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context);
+ setWillNotDraw(false);
+
+ final TypedArray keyboardViewAttr = context.obtainStyledAttributes(
+ attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
+ final int gestureFloatingPreviewTextSize = keyboardViewAttr.getDimensionPixelSize(
+ R.styleable.KeyboardView_gestureFloatingPreviewTextSize, 0);
+ mGestureFloatingPreviewTextColor = keyboardViewAttr.getColor(
+ R.styleable.KeyboardView_gestureFloatingPreviewTextColor, 0);
+ mGestureFloatingPreviewTextOffset = keyboardViewAttr.getDimensionPixelOffset(
+ R.styleable.KeyboardView_gestureFloatingPreviewTextOffset, 0);
+ mGestureFloatingPreviewColor = keyboardViewAttr.getColor(
+ R.styleable.KeyboardView_gestureFloatingPreviewColor, 0);
+ mGestureFloatingPreviewHorizontalPadding = keyboardViewAttr.getDimension(
+ R.styleable.KeyboardView_gestureFloatingPreviewHorizontalPadding, 0.0f);
+ mGestureFloatingPreviewVerticalPadding = keyboardViewAttr.getDimension(
+ R.styleable.KeyboardView_gestureFloatingPreviewVerticalPadding, 0.0f);
+ mGestureFloatingPreviewRoundRadius = keyboardViewAttr.getDimension(
+ R.styleable.KeyboardView_gestureFloatingPreviewRoundRadius, 0.0f);
+ final int gestureFloatingPreviewTextLingerTimeout = keyboardViewAttr.getInt(
+ R.styleable.KeyboardView_gestureFloatingPreviewTextLingerTimeout, 0);
+ mGesturePreviewTrailParams = new Params(keyboardViewAttr);
+ keyboardViewAttr.recycle();
+
+ mDrawingHandler = new DrawingHandler(this, mGesturePreviewTrailParams,
+ gestureFloatingPreviewTextLingerTimeout);
+
+ final Paint gesturePaint = new Paint();
+ gesturePaint.setAntiAlias(true);
+ gesturePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
+ mGesturePaint = gesturePaint;
+
+ final Paint textPaint = new Paint();
+ textPaint.setAntiAlias(true);
+ textPaint.setTextAlign(Align.CENTER);
+ textPaint.setTextSize(gestureFloatingPreviewTextSize);
+ mTextPaint = textPaint;
+ final Rect textRect = new Rect();
+ textPaint.getTextBounds(TEXT_HEIGHT_REFERENCE_CHAR, 0, 1, textRect);
+ mGestureFloatingPreviewTextHeight = textRect.height();
+
+ final Paint layerPaint = new Paint();
+ layerPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
+ setLayerType(LAYER_TYPE_HARDWARE, layerPaint);
+ }
+
+ public void setKeyboardViewGeometry(final int x, final int y, final int w, final int h) {
+ mKeyboardViewOriginX = x;
+ mKeyboardViewOriginY = y;
+ mOffscreenOffsetY = (int)(h * EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO);
+ mOffscreenWidth = w;
+ mOffscreenHeight = mOffscreenOffsetY + h;
+ }
+
+ public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail,
+ final boolean drawsGestureFloatingPreviewText) {
+ mDrawsGesturePreviewTrail = drawsGesturePreviewTrail;
+ mDrawsGestureFloatingPreviewText = drawsGestureFloatingPreviewText;
+ }
+
+ public void invalidatePointer(final PointerTracker tracker, final boolean isOldestTracker) {
+ final boolean needsToUpdateLastPointer =
+ isOldestTracker && mDrawsGestureFloatingPreviewText;
+ if (needsToUpdateLastPointer) {
+ mLastPointerX = tracker.getLastX();
+ mLastPointerY = tracker.getLastY();
+ }
+
+ if (mDrawsGesturePreviewTrail) {
+ GesturePreviewTrail trail;
+ synchronized (mGesturePreviewTrails) {
+ trail = mGesturePreviewTrails.get(tracker.mPointerId);
+ if (trail == null) {
+ trail = new GesturePreviewTrail();
+ mGesturePreviewTrails.put(tracker.mPointerId, trail);
+ }
+ }
+ trail.addStroke(tracker.getGestureStrokeWithPreviewPoints(), tracker.getDownTime());
+ }
+
+ // TODO: Should narrow the invalidate region.
+ if (mDrawsGesturePreviewTrail || needsToUpdateLastPointer) {
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ freeOffscreenBuffer();
+ }
+
+ private void freeOffscreenBuffer() {
+ if (mOffscreenBuffer != null) {
+ mOffscreenBuffer.recycle();
+ mOffscreenBuffer = null;
+ }
+ }
+
+ private void mayAllocateOffscreenBuffer() {
+ if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == mOffscreenWidth
+ && mOffscreenBuffer.getHeight() == mOffscreenHeight) {
+ return;
+ }
+ freeOffscreenBuffer();
+ mOffscreenBuffer = Bitmap.createBitmap(
+ mOffscreenWidth, mOffscreenHeight, Bitmap.Config.ARGB_8888);
+ mOffscreenCanvas.setBitmap(mOffscreenBuffer);
+ }
+
+ @Override
+ public void onDraw(final Canvas canvas) {
+ super.onDraw(canvas);
+ if (mDrawsGesturePreviewTrail) {
+ mayAllocateOffscreenBuffer();
+ // Draw gesture trails to offscreen buffer.
+ final boolean needsUpdatingGesturePreviewTrail = drawGestureTrails(
+ mOffscreenCanvas, mGesturePaint, mOffscreenDirtyRect);
+ // Transfer offscreen buffer to screen.
+ if (!mOffscreenDirtyRect.isEmpty()) {
+ final int offsetY = mKeyboardViewOriginY - mOffscreenOffsetY;
+ canvas.translate(mKeyboardViewOriginX, offsetY);
+ canvas.drawBitmap(mOffscreenBuffer, mOffscreenDirtyRect, mOffscreenDirtyRect,
+ mGesturePaint);
+ canvas.translate(-mKeyboardViewOriginX, -offsetY);
+ // Note: Defer clearing the dirty rectangle here because we will get cleared
+ // rectangle on the canvas.
+ }
+ if (needsUpdatingGesturePreviewTrail) {
+ mDrawingHandler.postUpdateGestureTrailPreview();
+ }
+ }
+ if (mDrawsGestureFloatingPreviewText) {
+ canvas.translate(mKeyboardViewOriginX, mKeyboardViewOriginY);
+ drawGestureFloatingPreviewText(canvas, mGestureFloatingPreviewText);
+ canvas.translate(-mKeyboardViewOriginX, -mKeyboardViewOriginY);
+ }
+ }
+
+ private boolean drawGestureTrails(final Canvas offscreenCanvas, final Paint paint,
+ final Rect dirtyRect) {
+ // Clear previous dirty rectangle.
+ if (!dirtyRect.isEmpty()) {
+ paint.setColor(Color.TRANSPARENT);
+ paint.setStyle(Paint.Style.FILL);
+ offscreenCanvas.drawRect(dirtyRect, paint);
+ }
+ dirtyRect.setEmpty();
+
+ // Draw gesture trails to offscreen buffer.
+ offscreenCanvas.translate(0, mOffscreenOffsetY);
+ boolean needsUpdatingGesturePreviewTrail = false;
+ synchronized (mGesturePreviewTrails) {
+ // Trails count == fingers count that have ever been active.
+ final int trailsCount = mGesturePreviewTrails.size();
+ for (int index = 0; index < trailsCount; index++) {
+ final GesturePreviewTrail trail = mGesturePreviewTrails.valueAt(index);
+ needsUpdatingGesturePreviewTrail |=
+ trail.drawGestureTrail(offscreenCanvas, paint,
+ mGesturePreviewTrailBoundsRect, mGesturePreviewTrailParams);
+ // {@link #mGesturePreviewTrailBoundsRect} has bounding box of the trail.
+ dirtyRect.union(mGesturePreviewTrailBoundsRect);
+ }
+ }
+ offscreenCanvas.translate(0, -mOffscreenOffsetY);
+
+ // Clip dirty rectangle with offscreen buffer width/height.
+ dirtyRect.offset(0, mOffscreenOffsetY);
+ clipRect(dirtyRect, 0, 0, mOffscreenWidth, mOffscreenHeight);
+ return needsUpdatingGesturePreviewTrail;
+ }
+
+ private static void clipRect(final Rect out, final int left, final int top, final int right,
+ final int bottom) {
+ out.set(Math.max(out.left, left), Math.max(out.top, top), Math.min(out.right, right),
+ Math.min(out.bottom, bottom));
+ }
+
+ public void setGestureFloatingPreviewText(final String gestureFloatingPreviewText) {
+ if (!mDrawsGestureFloatingPreviewText) return;
+ mGestureFloatingPreviewText = gestureFloatingPreviewText;
+ invalidate();
+ }
+
+ public void dismissGestureFloatingPreviewText() {
+ mDrawingHandler.dismissGestureFloatingPreviewText();
+ }
+
+ private void drawGestureFloatingPreviewText(final Canvas canvas,
+ final String gestureFloatingPreviewText) {
+ if (TextUtils.isEmpty(gestureFloatingPreviewText)) {
+ return;
+ }
+
+ final Paint paint = mTextPaint;
+ final RectF rectangle = mGestureFloatingPreviewRectangle;
+ // TODO: Figure out how we should deal with the floating preview text with multiple moving
+ // fingers.
+
+ // Paint the round rectangle background.
+ final int textHeight = mGestureFloatingPreviewTextHeight;
+ final float textWidth = paint.measureText(gestureFloatingPreviewText);
+ final float hPad = mGestureFloatingPreviewHorizontalPadding;
+ final float vPad = mGestureFloatingPreviewVerticalPadding;
+ final float rectWidth = textWidth + hPad * 2.0f;
+ final float rectHeight = textHeight + vPad * 2.0f;
+ final int canvasWidth = canvas.getWidth();
+ final float rectX = Math.min(Math.max(mLastPointerX - rectWidth / 2.0f, 0.0f),
+ canvasWidth - rectWidth);
+ final float rectY = mLastPointerY - mGestureFloatingPreviewTextOffset - rectHeight;
+ rectangle.set(rectX, rectY, rectX + rectWidth, rectY + rectHeight);
+ final float round = mGestureFloatingPreviewRoundRadius;
+ paint.setColor(mGestureFloatingPreviewColor);
+ canvas.drawRoundRect(rectangle, round, round, paint);
+ // Paint the text preview
+ paint.setColor(mGestureFloatingPreviewTextColor);
+ final float textX = rectX + hPad + textWidth / 2.0f;
+ final float textY = rectY + vPad + textHeight;
+ canvas.drawText(gestureFloatingPreviewText, textX, textY, paint);
+ }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java b/java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java
index edb40c8e7..90db73dee 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/ShiftKeyState.java
@@ -18,7 +18,7 @@ package com.android.inputmethod.keyboard.internal;
import android.util.Log;
-/* package */ class ShiftKeyState extends ModifierKeyState {
+/* package */ final class ShiftKeyState extends ModifierKeyState {
private static final int PRESSING_ON_SHIFTED = 3; // both temporary shifted & shift locked
private static final int IGNORING = 4;
diff --git a/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java b/java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java
index 107138395..c53428fe5 100644
--- a/java/src/com/android/inputmethod/keyboard/SuddenJumpingTouchEventHandler.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/SuddenJumpingTouchEventHandler.java
@@ -14,19 +14,21 @@
* the License.
*/
-package com.android.inputmethod.keyboard;
+package com.android.inputmethod.keyboard.internal;
import android.content.Context;
import android.util.Log;
import android.view.MotionEvent;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResearchLogger;
-import com.android.inputmethod.latin.Utils;
+import com.android.inputmethod.latin.ResourceUtils;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
-public class SuddenJumpingTouchEventHandler {
+public final class SuddenJumpingTouchEventHandler {
private static final String TAG = SuddenJumpingTouchEventHandler.class.getSimpleName();
private static boolean DEBUG_MODE = LatinImeLogger.sDBG;
@@ -51,7 +53,7 @@ public class SuddenJumpingTouchEventHandler {
public SuddenJumpingTouchEventHandler(Context context, ProcessMotionEvent view) {
mView = view;
- mNeedsSuddenJumpingHack = Boolean.parseBoolean(Utils.getDeviceOverrideValue(
+ mNeedsSuddenJumpingHack = Boolean.parseBoolean(ResourceUtils.getDeviceOverrideValue(
context.getResources(), R.array.sudden_jumping_touch_event_device_list, "false"));
}
@@ -70,7 +72,7 @@ public class SuddenJumpingTouchEventHandler {
* 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 LatinKeyboardView}.
+ * {@link MainKeyboardView}.
*/
private boolean handleSuddenJumping(MotionEvent me) {
if (!mNeedsSuddenJumpingHack)
diff --git a/java/src/com/android/inputmethod/keyboard/internal/TouchPositionCorrection.java b/java/src/com/android/inputmethod/keyboard/internal/TouchPositionCorrection.java
new file mode 100644
index 000000000..d8950a713
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/TouchPositionCorrection.java
@@ -0,0 +1,95 @@
+/*
+ * 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.keyboard.internal;
+
+import com.android.inputmethod.latin.LatinImeLogger;
+
+public final class TouchPositionCorrection {
+ private static final int TOUCH_POSITION_CORRECTION_RECORD_SIZE = 3;
+
+ private boolean mEnabled;
+ private float[] mXs;
+ private float[] mYs;
+ private float[] mRadii;
+
+ public void load(final String[] data) {
+ final int dataLength = data.length;
+ if (dataLength % TOUCH_POSITION_CORRECTION_RECORD_SIZE != 0) {
+ if (LatinImeLogger.sDBG) {
+ throw new RuntimeException(
+ "the size of touch position correction data is invalid");
+ }
+ return;
+ }
+
+ final int length = dataLength / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
+ mXs = new float[length];
+ mYs = new float[length];
+ mRadii = new float[length];
+ try {
+ for (int i = 0; i < dataLength; ++i) {
+ final int type = i % TOUCH_POSITION_CORRECTION_RECORD_SIZE;
+ final int index = i / TOUCH_POSITION_CORRECTION_RECORD_SIZE;
+ final float value = Float.parseFloat(data[i]);
+ if (type == 0) {
+ mXs[index] = value;
+ } else if (type == 1) {
+ mYs[index] = value;
+ } else {
+ mRadii[index] = value;
+ }
+ }
+ mEnabled = dataLength > 0;
+ } catch (NumberFormatException e) {
+ if (LatinImeLogger.sDBG) {
+ throw new RuntimeException(
+ "the number format for touch position correction data is invalid");
+ }
+ mEnabled = false;
+ mXs = null;
+ mYs = null;
+ mRadii = null;
+ }
+ }
+
+ // For test only
+ public void setEnabled(final boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ public boolean isValid() {
+ return mEnabled;
+ }
+
+ public int getRows() {
+ return mRadii.length;
+ }
+
+ public float getX(final int row) {
+ return 0.0f;
+ // Touch position correction data for X coordinate is obsolete.
+ // return mXs[row];
+ }
+
+ public float getY(final int row) {
+ return mYs[row];
+ }
+
+ public float getRadius(final int row) {
+ return mRadii[row];
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
index f8f1395b3..509fc1ba3 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
@@ -27,22 +27,22 @@ import android.view.inputmethod.InputMethodSubtype;
import java.util.ArrayList;
-public class AdditionalSubtype {
+public final class AdditionalSubtype {
private static final InputMethodSubtype[] EMPTY_SUBTYPE_ARRAY = new InputMethodSubtype[0];
private AdditionalSubtype() {
// This utility class is not publicly instantiable.
}
- public static boolean isAdditionalSubtype(InputMethodSubtype subtype) {
+ public static boolean isAdditionalSubtype(final InputMethodSubtype subtype) {
return subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE);
}
private static final String LOCALE_AND_LAYOUT_SEPARATOR = ":";
- public static final String PREF_SUBTYPE_SEPARATOR = ";";
+ private static final String PREF_SUBTYPE_SEPARATOR = ";";
- public static InputMethodSubtype createAdditionalSubtype(
- String localeString, String keyboardLayoutSetName, String extraValue) {
+ public static InputMethodSubtype createAdditionalSubtype(final String localeString,
+ final String keyboardLayoutSetName, final String extraValue) {
final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
final String layoutDisplayNameExtraValue;
if (Build.VERSION.SDK_INT >= /* JELLY_BEAN */ 15
@@ -62,7 +62,7 @@ public class AdditionalSubtype {
layoutExtraValue + "," + additionalSubtypeExtraValue, false, false);
}
- public static String getPrefSubtype(InputMethodSubtype subtype) {
+ public static String getPrefSubtype(final InputMethodSubtype subtype) {
final String localeString = subtype.getLocale();
final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
@@ -74,7 +74,7 @@ public class AdditionalSubtype {
: basePrefSubtype + LOCALE_AND_LAYOUT_SEPARATOR + extraValue;
}
- public static InputMethodSubtype createAdditionalSubtype(String prefSubtype) {
+ public static InputMethodSubtype createAdditionalSubtype(final String prefSubtype) {
final String elems[] = prefSubtype.split(LOCALE_AND_LAYOUT_SEPARATOR);
if (elems.length < 2 || elems.length > 3) {
throw new RuntimeException("Unknown additional subtype specified: " + prefSubtype);
@@ -85,13 +85,13 @@ public class AdditionalSubtype {
return createAdditionalSubtype(localeString, keyboardLayoutSetName, extraValue);
}
- public static InputMethodSubtype[] createAdditionalSubtypesArray(String prefSubtypes) {
+ public static InputMethodSubtype[] createAdditionalSubtypesArray(final String prefSubtypes) {
if (TextUtils.isEmpty(prefSubtypes)) {
return EMPTY_SUBTYPE_ARRAY;
}
final String[] prefSubtypeArray = prefSubtypes.split(PREF_SUBTYPE_SEPARATOR);
final ArrayList<InputMethodSubtype> subtypesList =
- new ArrayList<InputMethodSubtype>(prefSubtypeArray.length);
+ CollectionUtils.newArrayList(prefSubtypeArray.length);
for (final String prefSubtype : prefSubtypeArray) {
final InputMethodSubtype subtype = createAdditionalSubtype(prefSubtype);
if (subtype.getNameResId() == SubtypeLocale.UNKNOWN_KEYBOARD_LAYOUT) {
@@ -103,4 +103,32 @@ public class AdditionalSubtype {
}
return subtypesList.toArray(new InputMethodSubtype[subtypesList.size()]);
}
+
+ public static String createPrefSubtypes(final InputMethodSubtype[] subtypes) {
+ if (subtypes == null || subtypes.length == 0) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (final InputMethodSubtype subtype : subtypes) {
+ if (sb.length() > 0) {
+ sb.append(PREF_SUBTYPE_SEPARATOR);
+ }
+ sb.append(getPrefSubtype(subtype));
+ }
+ return sb.toString();
+ }
+
+ public static String createPrefSubtypes(final String[] prefSubtypes) {
+ if (prefSubtypes == null || prefSubtypes.length == 0) {
+ return "";
+ }
+ final StringBuilder sb = new StringBuilder();
+ for (final String prefSubtype : prefSubtypes) {
+ if (sb.length() > 0) {
+ sb.append(PREF_SUBTYPE_SEPARATOR);
+ }
+ sb.append(prefSubtype);
+ }
+ return sb.toString();
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
index 779a38823..d12607721 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtypeSettings.java
@@ -49,7 +49,7 @@ import com.android.inputmethod.compat.CompatUtils;
import java.util.ArrayList;
import java.util.TreeSet;
-public class AdditionalSubtypeSettings extends PreferenceFragment {
+public final class AdditionalSubtypeSettings extends PreferenceFragment {
private SharedPreferences mPrefs;
private SubtypeLocaleAdapter mSubtypeLocaleAdapter;
private KeyboardLayoutSetAdapter mKeyboardLayoutSetAdapter;
@@ -63,13 +63,13 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
private static final String KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN =
"is_subtype_enabler_notification_dialog_open";
private static final String KEY_SUBTYPE_FOR_SUBTYPE_ENABLER = "subtype_for_subtype_enabler";
- static class SubtypeLocaleItem extends Pair<String, String>
+ static final class SubtypeLocaleItem extends Pair<String, String>
implements Comparable<SubtypeLocaleItem> {
- public SubtypeLocaleItem(String localeString, String displayName) {
+ public SubtypeLocaleItem(final String localeString, final String displayName) {
super(localeString, displayName);
}
- public SubtypeLocaleItem(String localeString) {
+ public SubtypeLocaleItem(final String localeString) {
this(localeString, SubtypeLocale.getSubtypeLocaleDisplayName(localeString));
}
@@ -79,17 +79,17 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
@Override
- public int compareTo(SubtypeLocaleItem o) {
+ public int compareTo(final SubtypeLocaleItem o) {
return first.compareTo(o.first);
}
}
- static class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> {
- public SubtypeLocaleAdapter(Context context) {
+ static final class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> {
+ public SubtypeLocaleAdapter(final Context context) {
super(context, android.R.layout.simple_spinner_item);
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- final TreeSet<SubtypeLocaleItem> items = new TreeSet<SubtypeLocaleItem>();
+ final TreeSet<SubtypeLocaleItem> items = CollectionUtils.newTreeSet();
final InputMethodInfo imi = ImfUtils.getInputMethodInfoOfThisIme(context);
final int count = imi.getSubtypeCount();
for (int i = 0; i < count; i++) {
@@ -102,7 +102,8 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
addAll(items);
}
- public static SubtypeLocaleItem createItem(Context context, String localeString) {
+ public static SubtypeLocaleItem createItem(final Context context,
+ final String localeString) {
if (localeString.equals(SubtypeLocale.NO_LANGUAGE)) {
final String displayName = context.getString(R.string.subtype_no_language);
return new SubtypeLocaleItem(localeString, displayName);
@@ -112,8 +113,8 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
}
- static class KeyboardLayoutSetItem extends Pair<String, String> {
- public KeyboardLayoutSetItem(InputMethodSubtype subtype) {
+ static final class KeyboardLayoutSetItem extends Pair<String, String> {
+ public KeyboardLayoutSetItem(final InputMethodSubtype subtype) {
super(SubtypeLocale.getKeyboardLayoutSetName(subtype),
SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype));
}
@@ -124,8 +125,8 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
}
- static class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> {
- public KeyboardLayoutSetAdapter(Context context) {
+ static final class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> {
+ public KeyboardLayoutSetAdapter(final Context context) {
super(context, android.R.layout.simple_spinner_item);
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
@@ -147,7 +148,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter();
}
- static class SubtypePreference extends DialogPreference
+ static final class SubtypePreference extends DialogPreference
implements DialogInterface.OnCancelListener {
private static final String KEY_PREFIX = "subtype_pref_";
private static final String KEY_NEW_SUBTYPE = KEY_PREFIX + "new";
@@ -159,13 +160,13 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
private Spinner mSubtypeLocaleSpinner;
private Spinner mKeyboardLayoutSetSpinner;
- public static SubtypePreference newIncompleteSubtypePreference(
- Context context, SubtypeDialogProxy proxy) {
+ public static SubtypePreference newIncompleteSubtypePreference(final Context context,
+ final SubtypeDialogProxy proxy) {
return new SubtypePreference(context, null, proxy);
}
- public SubtypePreference(Context context, InputMethodSubtype subtype,
- SubtypeDialogProxy proxy) {
+ public SubtypePreference(final Context context, final InputMethodSubtype subtype,
+ final SubtypeDialogProxy proxy) {
super(context, null);
setDialogLayoutResource(R.layout.additional_subtype_dialog);
setPersistent(false);
@@ -185,7 +186,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
return mSubtype;
}
- public void setSubtype(InputMethodSubtype subtype) {
+ public void setSubtype(final InputMethodSubtype subtype) {
mPreviousSubtype = mSubtype;
mSubtype = subtype;
if (isIncomplete()) {
@@ -221,7 +222,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
@Override
- protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) {
final Context context = builder.getContext();
builder.setCancelable(true).setOnCancelListener(this);
if (isIncomplete()) {
@@ -239,7 +240,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
}
- private static void setSpinnerPosition(Spinner spinner, Object itemToSelect) {
+ private static void setSpinnerPosition(final Spinner spinner, final Object itemToSelect) {
final SpinnerAdapter adapter = spinner.getAdapter();
final int count = adapter.getCount();
for (int i = 0; i < count; i++) {
@@ -252,14 +253,14 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
@Override
- public void onCancel(DialogInterface dialog) {
+ public void onCancel(final DialogInterface dialog) {
if (isIncomplete()) {
mProxy.onRemovePressed(this);
}
}
@Override
- public void onClick(DialogInterface dialog, int which) {
+ public void onClick(final DialogInterface dialog, final int which) {
super.onClick(dialog, which);
switch (which) {
case DialogInterface.BUTTON_POSITIVE:
@@ -287,12 +288,12 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
}
- private static int getSpinnerPosition(Spinner spinner) {
+ private static int getSpinnerPosition(final Spinner spinner) {
if (spinner == null) return -1;
return spinner.getSelectedItemPosition();
}
- private static void setSpinnerPosition(Spinner spinner, int position) {
+ private static void setSpinnerPosition(final Spinner spinner, final int position) {
if (spinner == null || position < 0) return;
spinner.setSelection(position);
}
@@ -313,7 +314,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
@Override
- protected void onRestoreInstanceState(Parcelable state) {
+ protected void onRestoreInstanceState(final Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
@@ -326,24 +327,24 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
setSubtype(myState.mSubtype);
}
- static class SavedState extends Preference.BaseSavedState {
+ static final class SavedState extends Preference.BaseSavedState {
InputMethodSubtype mSubtype;
int mSubtypeLocaleSelectedPos;
int mKeyboardLayoutSetSelectedPos;
- public SavedState(Parcelable superState) {
+ public SavedState(final Parcelable superState) {
super(superState);
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
+ public void writeToParcel(final Parcel dest, final int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(mSubtypeLocaleSelectedPos);
dest.writeInt(mKeyboardLayoutSetSelectedPos);
dest.writeParcelable(mSubtype, 0);
}
- public SavedState(Parcel source) {
+ public SavedState(final Parcel source) {
super(source);
mSubtypeLocaleSelectedPos = source.readInt();
mKeyboardLayoutSetSelectedPos = source.readInt();
@@ -354,12 +355,12 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
public static final Parcelable.Creator<SavedState> CREATOR =
new Parcelable.Creator<SavedState>() {
@Override
- public SavedState createFromParcel(Parcel source) {
+ public SavedState createFromParcel(final Parcel source) {
return new SavedState(source);
}
@Override
- public SavedState[] newArray(int size) {
+ public SavedState[] newArray(final int size) {
return new SavedState[size];
}
};
@@ -371,7 +372,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
@Override
- public void onCreate(Bundle savedInstanceState) {
+ public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.additional_subtype_settings);
@@ -381,7 +382,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
@Override
- public void onActivityCreated(Bundle savedInstanceState) {
+ public void onActivityCreated(final Bundle savedInstanceState) {
final Context context = getActivity();
mSubtypeLocaleAdapter = new SubtypeLocaleAdapter(context);
mKeyboardLayoutSetAdapter = new KeyboardLayoutSetAdapter(context);
@@ -411,7 +412,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
@Override
- public void onSaveInstanceState(Bundle outState) {
+ public void onSaveInstanceState(final Bundle outState) {
super.onSaveInstanceState(outState);
if (mIsAddingNewSubtype) {
outState.putBoolean(KEY_IS_ADDING_NEW_SUBTYPE, true);
@@ -426,7 +427,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
private final SubtypeDialogProxy mSubtypeProxy = new SubtypeDialogProxy() {
@Override
- public void onRemovePressed(SubtypePreference subtypePref) {
+ public void onRemovePressed(final SubtypePreference subtypePref) {
mIsAddingNewSubtype = false;
final PreferenceGroup group = getPreferenceScreen();
group.removePreference(subtypePref);
@@ -434,7 +435,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
@Override
- public void onSavePressed(SubtypePreference subtypePref) {
+ public void onSavePressed(final SubtypePreference subtypePref) {
final InputMethodSubtype subtype = subtypePref.getSubtype();
if (!subtypePref.hasBeenModified()) {
return;
@@ -453,7 +454,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
@Override
- public void onAddPressed(SubtypePreference subtypePref) {
+ public void onAddPressed(final SubtypePreference subtypePref) {
mIsAddingNewSubtype = false;
final InputMethodSubtype subtype = subtypePref.getSubtype();
if (findDuplicatedSubtype(subtype) == null) {
@@ -481,7 +482,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
};
- private void showSubtypeAlreadyExistsToast(InputMethodSubtype subtype) {
+ private void showSubtypeAlreadyExistsToast(final InputMethodSubtype subtype) {
final Context context = getActivity();
final Resources res = context.getResources();
final String message = res.getString(R.string.custom_input_style_already_exists,
@@ -489,14 +490,15 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}
- private InputMethodSubtype findDuplicatedSubtype(InputMethodSubtype subtype) {
+ private InputMethodSubtype findDuplicatedSubtype(final InputMethodSubtype subtype) {
final String localeString = subtype.getLocale();
final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
return ImfUtils.findSubtypeByLocaleAndKeyboardLayoutSet(
getActivity(), localeString, keyboardLayoutSetName);
}
- private AlertDialog createDialog(SubtypePreference subtypePref) {
+ private AlertDialog createDialog(
+ @SuppressWarnings("unused") final SubtypePreference subtypePref) {
final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.custom_input_styles_title)
.setMessage(R.string.custom_input_style_note_message)
@@ -519,7 +521,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
return builder.create();
}
- private void setPrefSubtypes(String prefSubtypes, Context context) {
+ private void setPrefSubtypes(final String prefSubtypes, final Context context) {
final PreferenceGroup group = getPreferenceScreen();
group.removeAll();
final InputMethodSubtype[] subtypesArray =
@@ -533,7 +535,7 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
private InputMethodSubtype[] getSubtypes() {
final PreferenceGroup group = getPreferenceScreen();
- final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
+ final ArrayList<InputMethodSubtype> subtypes = CollectionUtils.newArrayList();
final int count = group.getPreferenceCount();
for (int i = 0; i < count; i++) {
final Preference pref = group.getPreference(i);
@@ -547,23 +549,12 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
return subtypes.toArray(new InputMethodSubtype[subtypes.size()]);
}
- private String getPrefSubtypes(InputMethodSubtype[] subtypes) {
- final StringBuilder sb = new StringBuilder();
- for (final InputMethodSubtype subtype : subtypes) {
- if (sb.length() > 0) {
- sb.append(AdditionalSubtype.PREF_SUBTYPE_SEPARATOR);
- }
- sb.append(AdditionalSubtype.getPrefSubtype(subtype));
- }
- return sb.toString();
- }
-
@Override
public void onPause() {
super.onPause();
final String oldSubtypes = SettingsValues.getPrefAdditionalSubtypes(mPrefs, getResources());
final InputMethodSubtype[] subtypes = getSubtypes();
- final String prefSubtypes = getPrefSubtypes(subtypes);
+ final String prefSubtypes = AdditionalSubtype.createPrefSubtypes(subtypes);
if (prefSubtypes.equals(oldSubtypes)) {
return;
}
@@ -578,13 +569,13 @@ public class AdditionalSubtypeSettings extends PreferenceFragment {
}
@Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
final MenuItem addSubtypeMenu = menu.add(0, MENU_ADD_SUBTYPE, 0, R.string.add_style);
addSubtypeMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
}
@Override
- public boolean onOptionsItemSelected(MenuItem item) {
+ public boolean onOptionsItemSelected(final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == MENU_ADD_SUBTYPE) {
final SubtypePreference newSubtype =
diff --git a/java/src/com/android/inputmethod/latin/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
index 3549a1561..29c733ba6 100644
--- a/java/src/com/android/inputmethod/latin/AssetFileAddress.java
+++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
@@ -24,7 +24,7 @@ import java.io.File;
* the package file. Open it correctly thus requires the name of the package it is in, but
* also the offset in the file and the length of this data. This class encapsulates these three.
*/
-class AssetFileAddress {
+final class AssetFileAddress {
public final String mFilename;
public final long mOffset;
public final long mLength;
diff --git a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
index 55664d411..59ef5e09f 100644
--- a/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
+++ b/java/src/com/android/inputmethod/latin/AudioAndHapticFeedbackManager.java
@@ -30,7 +30,7 @@ import com.android.inputmethod.latin.VibratorUtils;
* It offers a consistent and simple interface that allows LatinIME to forget about the
* complexity of settings and the like.
*/
-public class AudioAndHapticFeedbackManager {
+public final class AudioAndHapticFeedbackManager {
final private SettingsValues mSettingsValues;
final private AudioManager mAudioManager;
final private VibratorUtils mVibratorUtils;
diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java
index e0452483c..84fad158f 100644
--- a/java/src/com/android/inputmethod/latin/AutoCorrection.java
+++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java
@@ -21,34 +21,17 @@ import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import android.text.TextUtils;
import android.util.Log;
-import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
-public class AutoCorrection {
+public final class AutoCorrection {
private static final boolean DBG = LatinImeLogger.sDBG;
private static final String TAG = AutoCorrection.class.getSimpleName();
+ private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
private AutoCorrection() {
// Purely static class: can't instantiate.
}
- public static CharSequence computeAutoCorrectionWord(
- final ConcurrentHashMap<String, Dictionary> dictionaries,
- final WordComposer wordComposer, final ArrayList<SuggestedWordInfo> suggestions,
- final CharSequence consideredWord, final float autoCorrectionThreshold,
- final CharSequence whitelistedWord) {
- if (hasAutoCorrectionForWhitelistedWord(whitelistedWord)) {
- return whitelistedWord;
- } else if (hasAutoCorrectionForConsideredWord(
- dictionaries, wordComposer, suggestions, consideredWord)) {
- return consideredWord;
- } else if (hasAutoCorrectionForBinaryDictionary(wordComposer, suggestions,
- consideredWord, autoCorrectionThreshold)) {
- return suggestions.get(0).mWord;
- }
- return null;
- }
-
public static boolean isValidWord(final ConcurrentHashMap<String, Dictionary> dictionaries,
CharSequence word, boolean ignoreCase) {
if (TextUtils.isEmpty(word)) {
@@ -56,7 +39,6 @@ public class AutoCorrection {
}
final CharSequence lowerCasedWord = word.toString().toLowerCase();
for (final String key : dictionaries.keySet()) {
- if (key.equals(Suggest.DICT_KEY_WHITELIST)) continue;
final Dictionary dictionary = dictionaries.get(key);
// It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
// managing to get null in here. Presumably the language is changing to a language with
@@ -81,7 +63,6 @@ public class AutoCorrection {
}
int maxFreq = -1;
for (final String key : dictionaries.keySet()) {
- if (key.equals(Suggest.DICT_KEY_WHITELIST)) continue;
final Dictionary dictionary = dictionaries.get(key);
if (null == dictionary) continue;
final int tempFreq = dictionary.getFrequency(word);
@@ -92,46 +73,26 @@ public class AutoCorrection {
return maxFreq;
}
- public static boolean allowsToBeAutoCorrected(
+ // Returns true if this is in any of the dictionaries.
+ public static boolean isInTheDictionary(
final ConcurrentHashMap<String, Dictionary> dictionaries,
final CharSequence word, final boolean ignoreCase) {
- final WhitelistDictionary whitelistDictionary =
- (WhitelistDictionary)dictionaries.get(Suggest.DICT_KEY_WHITELIST);
- // If "word" is in the whitelist dictionary, it should not be auto corrected.
- if (whitelistDictionary != null
- && whitelistDictionary.shouldForciblyAutoCorrectFrom(word)) {
- return true;
- }
- return !isValidWord(dictionaries, word, ignoreCase);
- }
-
- private static boolean hasAutoCorrectionForWhitelistedWord(CharSequence whiteListedWord) {
- return whiteListedWord != null;
- }
-
- private static boolean hasAutoCorrectionForConsideredWord(
- final ConcurrentHashMap<String, Dictionary> dictionaries,
- final WordComposer wordComposer, final ArrayList<SuggestedWordInfo> suggestions,
- final CharSequence consideredWord) {
- if (TextUtils.isEmpty(consideredWord)) return false;
- return wordComposer.size() > 1 && suggestions.size() > 0
- && !allowsToBeAutoCorrected(dictionaries, consideredWord, false);
+ return isValidWord(dictionaries, word, ignoreCase);
}
- private static boolean hasAutoCorrectionForBinaryDictionary(WordComposer wordComposer,
- ArrayList<SuggestedWordInfo> suggestions,
+ public static boolean suggestionExceedsAutoCorrectionThreshold(SuggestedWordInfo suggestion,
CharSequence consideredWord, float autoCorrectionThreshold) {
- if (wordComposer.size() > 1 && suggestions.size() > 0) {
- final SuggestedWordInfo autoCorrectionSuggestion = suggestions.get(0);
- //final int autoCorrectionSuggestionScore = sortedScores[0];
- final int autoCorrectionSuggestionScore = autoCorrectionSuggestion.mScore;
+ if (null != suggestion) {
+ // Shortlist a whitelisted word
+ if (suggestion.mKind == SuggestedWordInfo.KIND_WHITELIST) return true;
+ final int autoCorrectionSuggestionScore = suggestion.mScore;
// TODO: when the normalized score of the first suggestion is nearly equals to
// the normalized score of the second suggestion, behave less aggressive.
final float normalizedScore = BinaryDictionary.calcNormalizedScore(
- consideredWord.toString(), autoCorrectionSuggestion.mWord.toString(),
+ consideredWord.toString(), suggestion.mWord.toString(),
autoCorrectionSuggestionScore);
if (DBG) {
- Log.d(TAG, "Normalized " + consideredWord + "," + autoCorrectionSuggestion + ","
+ Log.d(TAG, "Normalized " + consideredWord + "," + suggestion + ","
+ autoCorrectionSuggestionScore + ", " + normalizedScore
+ "(" + autoCorrectionThreshold + ")");
}
@@ -139,10 +100,43 @@ public class AutoCorrection {
if (DBG) {
Log.d(TAG, "Auto corrected by S-threshold.");
}
- return true;
+ return !shouldBlockAutoCorrectionBySafetyNet(consideredWord.toString(),
+ suggestion.mWord);
}
}
return false;
}
+ // TODO: Resolve the inconsistencies between the native auto correction algorithms and
+ // this safety net
+ public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord,
+ final CharSequence suggestion) {
+ // Safety net for auto correction.
+ // Actually if we hit this safety net, it's a bug.
+ // If user selected aggressive auto correction mode, there is no need to use the safety
+ // net.
+ // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
+ // we should not use net because relatively edit distance can be big.
+ final int typedWordLength = typedWord.length();
+ if (typedWordLength < MINIMUM_SAFETY_NET_CHAR_LENGTH) {
+ return false;
+ }
+ final int maxEditDistanceOfNativeDictionary =
+ (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1;
+ final int distance = BinaryDictionary.editDistance(typedWord, suggestion.toString());
+ if (DBG) {
+ Log.d(TAG, "Autocorrected edit distance = " + distance
+ + ", " + maxEditDistanceOfNativeDictionary);
+ }
+ if (distance > maxEditDistanceOfNativeDictionary) {
+ if (DBG) {
+ Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestion);
+ Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. "
+ + "Turning off auto-correction.");
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/BackupAgent.java b/java/src/com/android/inputmethod/latin/BackupAgent.java
index ee070af75..0beb088ac 100644
--- a/java/src/com/android/inputmethod/latin/BackupAgent.java
+++ b/java/src/com/android/inputmethod/latin/BackupAgent.java
@@ -22,7 +22,7 @@ import android.app.backup.SharedPreferencesBackupHelper;
/**
* Backs up the Latin IME shared preferences.
*/
-public class BackupAgent extends BackupAgentHelper {
+public final class BackupAgent extends BackupAgentHelper {
@Override
public void onCreate() {
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index d0613bd72..7184f1d8a 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -18,16 +18,19 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.text.TextUtils;
+import android.util.SparseArray;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
/**
* Implements a static, compacted, binary dictionary of standard words.
*/
-public class BinaryDictionary extends Dictionary {
+public final class BinaryDictionary extends Dictionary {
public static final String DICTIONARY_PACK_AUTHORITY =
"com.android.inputmethod.latin.dictionarypack";
@@ -38,24 +41,46 @@ public class BinaryDictionary extends Dictionary {
* It is necessary to keep it at this value because some languages e.g. German have
* really long words.
*/
- public static final int MAX_WORD_LENGTH = 48;
+ public static final int MAX_WORD_LENGTH = Constants.Dictionary.MAX_WORD_LENGTH;
public static final int MAX_WORDS = 18;
+ public static final int MAX_SPACES = 16;
- private static final String TAG = "BinaryDictionary";
- private static final int MAX_BIGRAMS = 60;
+ private static final String TAG = BinaryDictionary.class.getSimpleName();
+ private static final int MAX_PREDICTIONS = 60;
+ private static final int MAX_RESULTS = Math.max(MAX_PREDICTIONS, MAX_WORDS);
private static final int TYPED_LETTER_MULTIPLIER = 2;
- private int mDicTypeId;
private long mNativeDict;
- private final int[] mInputCodes = new int[MAX_WORD_LENGTH];
- private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_WORDS];
- private final char[] mOutputChars_bigrams = new char[MAX_WORD_LENGTH * MAX_BIGRAMS];
- private final int[] mScores = new int[MAX_WORDS];
- private final int[] mBigramScores = new int[MAX_BIGRAMS];
+ private final Locale mLocale;
+ private final int[] mInputCodePoints = new int[MAX_WORD_LENGTH];
+ // TODO: The below should be int[] mOutputCodePoints
+ private final char[] mOutputChars = new char[MAX_WORD_LENGTH * MAX_RESULTS];
+ private final int[] mSpaceIndices = new int[MAX_SPACES];
+ private final int[] mOutputScores = new int[MAX_RESULTS];
+ private final int[] mOutputTypes = new int[MAX_RESULTS];
private final boolean mUseFullEditDistance;
+ private final SparseArray<DicTraverseSession> mDicTraverseSessions =
+ CollectionUtils.newSparseArray();
+
+ // TODO: There should be a way to remove used DicTraverseSession objects from
+ // {@code mDicTraverseSessions}.
+ private DicTraverseSession getTraverseSession(int traverseSessionId) {
+ synchronized(mDicTraverseSessions) {
+ DicTraverseSession traverseSession = mDicTraverseSessions.get(traverseSessionId);
+ if (traverseSession == null) {
+ traverseSession = mDicTraverseSessions.get(traverseSessionId);
+ if (traverseSession == null) {
+ traverseSession = new DicTraverseSession(mLocale, mNativeDict);
+ mDicTraverseSessions.put(traverseSessionId, traverseSession);
+ }
+ }
+ return traverseSession;
+ }
+ }
+
/**
* Constructor for the binary dictionary. This is supposed to be called from the
* dictionary factory.
@@ -65,14 +90,13 @@ public class BinaryDictionary extends Dictionary {
* @param offset the offset of the dictionary data within the file.
* @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
*/
public BinaryDictionary(final Context context,
final String filename, final long offset, final long length,
- final boolean useFullEditDistance, final Locale locale) {
- // Note: at the moment a binary dictionary is always of the "main" type.
- // Initializing this here will help transitioning out of the scheme where
- // the Suggest class knows everything about every single dictionary.
- mDicTypeId = Suggest.DIC_MAIN;
+ final boolean useFullEditDistance, final Locale locale, final String dictType) {
+ super(dictType);
+ mLocale = locale;
mUseFullEditDistance = useFullEditDistance;
loadDictionary(filename, offset, length);
}
@@ -82,121 +106,91 @@ public class BinaryDictionary extends Dictionary {
}
private native long openNative(String sourceDir, long dictOffset, long dictSize,
- int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, int maxWords);
+ int typedLetterMultiplier, int fullWordMultiplier, int maxWordLength, int maxWords,
+ int maxPredictions);
private native void closeNative(long dict);
- private native int getFrequencyNative(long dict, int[] word, int wordLength);
+ private native int getFrequencyNative(long dict, int[] word);
private native boolean isValidBigramNative(long dict, int[] word1, int[] word2);
- private native int getSuggestionsNative(long dict, long proximityInfo, int[] xCoordinates,
- int[] yCoordinates, int[] inputCodes, int codesSize, int[] prevWordForBigrams,
- boolean useFullEditDistance, char[] outputChars, int[] scores);
- private native int getBigramsNative(long dict, int[] prevWord, int prevWordLength,
- int[] inputCodes, int inputCodesLength, char[] outputChars, int[] scores,
- int maxWordLength, int maxBigrams);
- private static native float calcNormalizedScoreNative(
- char[] before, int beforeLength, char[] after, int afterLength, int score);
- private static native int editDistanceNative(
- char[] before, int beforeLength, char[] after, int afterLength);
-
+ private native int getSuggestionsNative(long dict, long proximityInfo, long traverseSession,
+ int[] xCoordinates, int[] yCoordinates, int[] times, int[] pointerIds,
+ int[] inputCodePoints, int codesSize, int commitPoint, boolean isGesture,
+ int[] prevWordCodePointArray, boolean useFullEditDistance, char[] outputChars,
+ int[] outputScores, int[] outputIndices, int[] outputTypes);
+ private static native float calcNormalizedScoreNative(char[] before, char[] after, int score);
+ private static native int editDistanceNative(char[] before, char[] after);
+
+ // TODO: Move native dict into session
private final void loadDictionary(String path, long startOffset, long length) {
- mNativeDict = openNative(path, startOffset, length,
- TYPED_LETTER_MULTIPLIER, FULL_WORD_SCORE_MULTIPLIER, MAX_WORD_LENGTH, MAX_WORDS);
+ mNativeDict = openNative(path, startOffset, length, TYPED_LETTER_MULTIPLIER,
+ FULL_WORD_SCORE_MULTIPLIER, MAX_WORD_LENGTH, MAX_WORDS, MAX_PREDICTIONS);
}
@Override
- public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
- if (mNativeDict == 0) return;
-
- int[] codePoints = StringUtils.toCodePointArray(previousWord.toString());
- Arrays.fill(mOutputChars_bigrams, (char) 0);
- Arrays.fill(mBigramScores, 0);
-
- int codesSize = codes.size();
- Arrays.fill(mInputCodes, -1);
- if (codesSize > 0) {
- mInputCodes[0] = codes.getCodeAt(0);
- }
-
- int count = getBigramsNative(mNativeDict, codePoints, codePoints.length, mInputCodes,
- codesSize, mOutputChars_bigrams, mBigramScores, MAX_WORD_LENGTH, MAX_BIGRAMS);
- if (count > MAX_BIGRAMS) {
- count = MAX_BIGRAMS;
- }
-
- for (int j = 0; j < count; ++j) {
- if (codesSize > 0 && mBigramScores[j] < 1) break;
- final int start = j * MAX_WORD_LENGTH;
- int len = 0;
- while (len < MAX_WORD_LENGTH && mOutputChars_bigrams[start + len] != 0) {
- ++len;
- }
- if (len > 0) {
- callback.addWord(mOutputChars_bigrams, start, len, mBigramScores[j],
- mDicTypeId, Dictionary.BIGRAM);
- }
- }
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo) {
+ return getSuggestionsWithSessionId(composer, prevWord, proximityInfo, 0);
}
- // proximityInfo and/or prevWordForBigrams may not be null.
@Override
- public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
- final WordCallback callback, final ProximityInfo proximityInfo) {
- final int count = getSuggestions(codes, prevWordForBigrams, proximityInfo, mOutputChars,
- mScores);
+ public ArrayList<SuggestedWordInfo> getSuggestionsWithSessionId(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo, int sessionId) {
+ if (!isValidDictionary()) return null;
+
+ Arrays.fill(mInputCodePoints, Constants.NOT_A_CODE);
+ // TODO: toLowerCase in the native code
+ final int[] prevWordCodePointArray = (null == prevWord)
+ ? null : StringUtils.toCodePointArray(prevWord.toString());
+ final int composerSize = composer.size();
+
+ final boolean isGesture = composer.isBatchMode();
+ if (composerSize <= 1 || !isGesture) {
+ if (composerSize > MAX_WORD_LENGTH - 1) return null;
+ for (int i = 0; i < composerSize; i++) {
+ mInputCodePoints[i] = composer.getCodeAt(i);
+ }
+ }
+ final InputPointers ips = composer.getInputPointers();
+ final int codesSize = isGesture ? ips.getPointerSize() : composerSize;
+ // proximityInfo and/or prevWordForBigrams may not be null.
+ final int tmpCount = getSuggestionsNative(mNativeDict,
+ proximityInfo.getNativeProximityInfo(), getTraverseSession(sessionId).getSession(),
+ ips.getXCoordinates(), ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(),
+ mInputCodePoints, codesSize, 0 /* commitPoint */, isGesture, prevWordCodePointArray,
+ mUseFullEditDistance, mOutputChars, mOutputScores, mSpaceIndices, mOutputTypes);
+ final int count = Math.min(tmpCount, MAX_PREDICTIONS);
+
+ final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
for (int j = 0; j < count; ++j) {
- if (mScores[j] < 1) break;
+ if (composerSize > 0 && mOutputScores[j] < 1) break;
final int start = j * MAX_WORD_LENGTH;
int len = 0;
while (len < MAX_WORD_LENGTH && mOutputChars[start + len] != 0) {
++len;
}
if (len > 0) {
- callback.addWord(mOutputChars, start, len, mScores[j], mDicTypeId,
- Dictionary.UNIGRAM);
+ final int score = SuggestedWordInfo.KIND_WHITELIST == mOutputTypes[j]
+ ? SuggestedWordInfo.MAX_SCORE : mOutputScores[j];
+ suggestions.add(new SuggestedWordInfo(
+ new String(mOutputChars, start, len), score, mOutputTypes[j], mDictType));
}
}
+ return suggestions;
}
/* package for test */ boolean isValidDictionary() {
return mNativeDict != 0;
}
- // proximityInfo may not be null.
- /* package for test */ int getSuggestions(final WordComposer codes,
- final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo,
- char[] outputChars, int[] scores) {
- if (!isValidDictionary()) return -1;
-
- final int codesSize = codes.size();
- // Won't deal with really long words.
- if (codesSize > MAX_WORD_LENGTH - 1) return -1;
-
- Arrays.fill(mInputCodes, WordComposer.NOT_A_CODE);
- for (int i = 0; i < codesSize; i++) {
- mInputCodes[i] = codes.getCodeAt(i);
- }
- Arrays.fill(outputChars, (char) 0);
- Arrays.fill(scores, 0);
-
- final int[] prevWordCodePointArray = null == prevWordForBigrams
- ? null : StringUtils.toCodePointArray(prevWordForBigrams.toString());
-
- // TODO: pass the previous word to native code
- return getSuggestionsNative(
- mNativeDict, proximityInfo.getNativeProximityInfo(),
- codes.getXCoordinates(), codes.getYCoordinates(), mInputCodes, codesSize,
- prevWordCodePointArray, mUseFullEditDistance, outputChars, scores);
- }
-
public static float calcNormalizedScore(String before, String after, int score) {
- return calcNormalizedScoreNative(before.toCharArray(), before.length(),
- after.toCharArray(), after.length(), score);
+ return calcNormalizedScoreNative(before.toCharArray(), after.toCharArray(), score);
}
public static int editDistance(String before, String after) {
- return editDistanceNative(
- before.toCharArray(), before.length(), after.toCharArray(), after.length());
+ if (before == null || after == null) {
+ throw new IllegalArgumentException();
+ }
+ return editDistanceNative(before.toCharArray(), after.toCharArray());
}
@Override
@@ -207,8 +201,8 @@ public class BinaryDictionary extends Dictionary {
@Override
public int getFrequency(CharSequence word) {
if (word == null) return -1;
- int[] chars = StringUtils.toCodePointArray(word.toString());
- return getFrequencyNative(mNativeDict, chars, chars.length);
+ int[] codePoints = StringUtils.toCodePointArray(word.toString());
+ return getFrequencyNative(mNativeDict, codePoints);
}
// TODO: Add a batch process version (isValidBigramMultiple?) to avoid excessive numbers of jni
@@ -221,11 +215,20 @@ public class BinaryDictionary extends Dictionary {
}
@Override
- public synchronized void close() {
+ public void close() {
+ synchronized (mDicTraverseSessions) {
+ final int sessionsSize = mDicTraverseSessions.size();
+ for (int index = 0; index < sessionsSize; ++index) {
+ final DicTraverseSession traverseSession = mDicTraverseSessions.valueAt(index);
+ if (traverseSession != null) {
+ traverseSession.close();
+ }
+ }
+ }
closeInternal();
}
- private void closeInternal() {
+ private synchronized void closeInternal() {
if (mNativeDict != 0) {
closeNative(mNativeDict);
mNativeDict = 0;
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 236c198ad..b0b65edb6 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -30,7 +30,6 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -40,14 +39,14 @@ import java.util.Locale;
* Group class for static methods to help with creation and getting of the binary dictionary
* file from the dictionary provider
*/
-public class BinaryDictionaryFileDumper {
+public final class BinaryDictionaryFileDumper {
private static final String TAG = BinaryDictionaryFileDumper.class.getSimpleName();
private static final boolean DEBUG = false;
/**
* The size of the temporary buffer to copy files.
*/
- private static final int FILE_READ_BUFFER_SIZE = 1024;
+ private static final int FILE_READ_BUFFER_SIZE = 8192;
// TODO: make the following data common with the native code
private static final byte[] MAGIC_NUMBER_VERSION_1 =
new byte[] { (byte)0x78, (byte)0xB1, (byte)0x00, (byte)0x00 };
@@ -99,7 +98,7 @@ public class BinaryDictionaryFileDumper {
}
try {
- final List<WordListInfo> list = new ArrayList<WordListInfo>();
+ final List<WordListInfo> list = CollectionUtils.newArrayList();
do {
final String wordListId = c.getString(0);
final String wordListLocale = c.getString(1);
@@ -150,7 +149,7 @@ public class BinaryDictionaryFileDumper {
final Uri.Builder wordListUriBuilder = getProviderUriBuilder(id);
final String finalFileName = BinaryDictionaryGetter.getCacheFileName(id, locale, context);
- final String tempFileName = finalFileName + ".tmp";
+ final String tempFileName = BinaryDictionaryGetter.getTempFileName(id, context);
for (int mode = MODE_MIN; mode <= MODE_MAX; ++mode) {
InputStream originalSourceStream = null;
@@ -201,6 +200,7 @@ public class BinaryDictionaryFileDumper {
outputStream.flush();
outputStream.close();
final File finalFile = new File(finalFileName);
+ finalFile.delete();
if (!outputFile.renameTo(finalFile)) {
throw new IOException("Can't move the file to its final name");
}
@@ -267,7 +267,7 @@ public class BinaryDictionaryFileDumper {
final ContentResolver resolver = context.getContentResolver();
final List<WordListInfo> idList = getWordListWordListInfos(locale, context,
hasDefaultWordList);
- final List<AssetFileAddress> fileAddressList = new ArrayList<AssetFileAddress>();
+ final List<AssetFileAddress> fileAddressList = CollectionUtils.newArrayList();
for (WordListInfo id : idList) {
final AssetFileAddress afd = cacheWordList(id.mId, id.mLocale, resolver, context);
if (null != afd) {
@@ -287,6 +287,7 @@ public class BinaryDictionaryFileDumper {
* @param input the stream to be copied.
* @param output an output stream to copy the data to.
*/
+ // TODO: make output a BufferedOutputStream
private static void checkMagicAndCopyFileTo(final BufferedInputStream input,
final FileOutputStream output) throws FileNotFoundException, IOException {
// Check the magic number
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 063243e1b..c747dc673 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -16,6 +16,9 @@
package com.android.inputmethod.latin;
+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.NameNotFoundException;
@@ -23,6 +26,10 @@ import android.content.res.AssetFileDescriptor;
import android.util.Log;
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;
@@ -30,7 +37,7 @@ import java.util.Locale;
/**
* Helper class to get the address of a mmap'able dictionary file.
*/
-class BinaryDictionaryGetter {
+final class BinaryDictionaryGetter {
/**
* Used for Log actions from this class
@@ -51,6 +58,9 @@ class BinaryDictionaryGetter {
private static final String MAIN_DICTIONARY_CATEGORY = "main";
public static final String ID_CATEGORY_SEPARATOR = ":";
+ // The key considered to read the version attribute in a dictionary file.
+ private static String VERSION_KEY = "version";
+
// Prevents this from being instantiated
private BinaryDictionaryGetter() {}
@@ -154,6 +164,18 @@ class BinaryDictionaryGetter {
}
/**
+ * Generates a unique temporary file name in the app cache directory.
+ *
+ * This is unique as long as it doesn't get called twice in the same millisecond by the same
+ * thread, which should be more than enough for our purposes.
+ */
+ public static String getTempFileName(String id, Context context) {
+ final String fileName = replaceFileNameDangerousCharacters(id);
+ return context.getCacheDir() + File.separator + fileName + "."
+ + Thread.currentThread().getId() + "." + System.currentTimeMillis();
+ }
+
+ /**
* Returns a file address from a resource, or null if it cannot be opened.
*/
private static AssetFileAddress loadFallbackResource(final Context context,
@@ -168,7 +190,7 @@ class BinaryDictionaryGetter {
context.getApplicationInfo().sourceDir, afd.getStartOffset(), afd.getLength());
}
- static private class DictPackSettings {
+ private static final class DictPackSettings {
final SharedPreferences mDictPreferences;
public DictPackSettings(final Context context) {
Context dictPackContext = null;
@@ -227,7 +249,7 @@ class BinaryDictionaryGetter {
/**
* Utility class for the {@link #getCachedWordLists} method
*/
- private static class FileAndMatchLevel {
+ private static final class FileAndMatchLevel {
final File mFile;
final int mMatchLevel;
public FileAndMatchLevel(final File file, final int matchLevel) {
@@ -254,8 +276,7 @@ class BinaryDictionaryGetter {
final Context context) {
final File[] directoryList = getCachedDirectoryList(context);
if (null == directoryList) return EMPTY_FILE_ARRAY;
- final HashMap<String, FileAndMatchLevel> cacheFiles =
- new HashMap<String, FileAndMatchLevel>();
+ final HashMap<String, FileAndMatchLevel> cacheFiles = CollectionUtils.newHashMap();
for (File directory : directoryList) {
if (!directory.isDirectory()) continue;
final String dirLocale = getWordListIdFromFileName(directory.getName());
@@ -336,6 +357,57 @@ class BinaryDictionaryGetter {
return MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
}
+ // ## 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.
+ 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 String version = options.get(VERSION_KEY);
+ if (null == version) {
+ // No version in the options : the format is unexpected
+ return false;
+ }
+ // Version 18 is the first one to include the whitelist
+ // Obviously this is a big ## HACK ##
+ return Integer.parseInt(version) >= 18;
+ } catch (java.io.FileNotFoundException e) {
+ return false;
+ } catch (java.io.IOException e) {
+ return false;
+ } catch (NumberFormatException e) {
+ return false;
+ } catch (BufferUnderflowException e) {
+ return false;
+ } finally {
+ if (inStream != null) {
+ try {
+ inStream.close();
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
+ }
+ }
+
/**
* Returns a list of file addresses for a given locale, trying relevant methods in order.
*
@@ -362,18 +434,19 @@ class BinaryDictionaryGetter {
final DictPackSettings dictPackSettings = new DictPackSettings(context);
boolean foundMainDict = false;
- final ArrayList<AssetFileAddress> fileList = new ArrayList<AssetFileAddress>();
+ final ArrayList<AssetFileAddress> fileList = CollectionUtils.newArrayList();
// cachedWordLists may not be null, see doc for getCachedDictionaryList
for (final File f : cachedWordLists) {
final String wordListId = getWordListIdFromFileName(f.getName());
- if (isMainWordListId(wordListId)) {
+ final boolean canUse = f.canRead() && hackCanUseDictionaryFile(locale, f);
+ if (canUse && isMainWordListId(wordListId)) {
foundMainDict = true;
}
if (!dictPackSettings.isWordListActive(wordListId)) continue;
- if (f.canRead()) {
+ if (canUse) {
fileList.add(AssetFileAddress.makeFromFileName(f.getPath()));
} else {
- Log.e(TAG, "Found a cached dictionary file but cannot read it");
+ Log.e(TAG, "Found a cached dictionary file but cannot read or use it");
}
}
diff --git a/java/src/com/android/inputmethod/latin/BoundedTreeSet.java b/java/src/com/android/inputmethod/latin/BoundedTreeSet.java
new file mode 100644
index 000000000..7f7ff31c8
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/BoundedTreeSet.java
@@ -0,0 +1,49 @@
+/*
+ * 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 com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.TreeSet;
+
+/**
+ * A TreeSet that is bounded in size and throws everything that's smaller than its limit
+ */
+public final class BoundedTreeSet extends TreeSet<SuggestedWordInfo> {
+ private final int mCapacity;
+ public BoundedTreeSet(final Comparator<SuggestedWordInfo> comparator, final int capacity) {
+ super(comparator);
+ mCapacity = capacity;
+ }
+
+ @Override
+ public boolean add(final SuggestedWordInfo e) {
+ if (size() < mCapacity) return super.add(e);
+ if (comparator().compare(e, last()) > 0) return false;
+ super.add(e);
+ pollLast(); // removes the last element
+ return true;
+ }
+
+ @Override
+ public boolean addAll(final Collection<? extends SuggestedWordInfo> e) {
+ if (null == e) return false;
+ return super.addAll(e);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/CollectionUtils.java b/java/src/com/android/inputmethod/latin/CollectionUtils.java
new file mode 100644
index 000000000..c75f2df5c
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/CollectionUtils.java
@@ -0,0 +1,95 @@
+/*
+ * 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.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public final class CollectionUtils {
+ private CollectionUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ public static <K,V> HashMap<K,V> newHashMap() {
+ return new HashMap<K,V>();
+ }
+
+ public static <K,V> TreeMap<K,V> newTreeMap() {
+ return new TreeMap<K,V>();
+ }
+
+ public static <K, V> Map<K,V> newSynchronizedTreeMap() {
+ final TreeMap<K,V> treeMap = newTreeMap();
+ return Collections.synchronizedMap(treeMap);
+ }
+
+ public static <K,V> ConcurrentHashMap<K,V> newConcurrentHashMap() {
+ return new ConcurrentHashMap<K,V>();
+ }
+
+ public static <E> HashSet<E> newHashSet() {
+ return new HashSet<E>();
+ }
+
+ public static <E> TreeSet<E> newTreeSet() {
+ return new TreeSet<E>();
+ }
+
+ public static <E> ArrayList<E> newArrayList() {
+ return new ArrayList<E>();
+ }
+
+ public static <E> ArrayList<E> newArrayList(final int initialCapacity) {
+ return new ArrayList<E>(initialCapacity);
+ }
+
+ public static <E> ArrayList<E> newArrayList(final Collection<E> collection) {
+ return new ArrayList<E>(collection);
+ }
+
+ public static <E> LinkedList<E> newLinkedList() {
+ return new LinkedList<E>();
+ }
+
+ public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList() {
+ return new CopyOnWriteArrayList<E>();
+ }
+
+ public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList(
+ final Collection<E> collection) {
+ return new CopyOnWriteArrayList<E>(collection);
+ }
+
+ public static <E> CopyOnWriteArrayList<E> newCopyOnWriteArrayList(final E[] array) {
+ return new CopyOnWriteArrayList<E>(array);
+ }
+
+ public static <E> SparseArray<E> newSparseArray() {
+ return new SparseArray<E>();
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index e79db367c..57e12a64f 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -16,9 +16,14 @@
package com.android.inputmethod.latin;
-import android.view.inputmethod.EditorInfo;
-
public final class Constants {
+ public static final class Color {
+ /**
+ * The alpha value for fully opaque.
+ */
+ public final static int ALPHA_OPAQUE = 255;
+ }
+
public static final class ImeOption {
/**
* The private IME option used to indicate that no microphone should be shown for a given
@@ -47,7 +52,7 @@ public final class Constants {
* The private IME option used to indicate that the given text field needs ASCII code points
* input.
*
- * @deprecated Use {@link EditorInfo#IME_FLAG_FORCE_ASCII}.
+ * @deprecated Use EditorInfo#IME_FLAG_FORCE_ASCII.
*/
@SuppressWarnings("dep-ann")
public static final String FORCE_ASCII = "forceAscii";
@@ -121,6 +126,21 @@ public final class Constants {
}
}
+ public static class Dictionary {
+ 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;
+
+ // See {@link KeyboardActionListener.Adapter#isInvalidCoordinate(int)}.
+ public static final int NOT_A_COORDINATE = -1;
+ public static final int SUGGESTION_STRIP_COORDINATE = -2;
+ public static final int SPELL_CHECKER_COORDINATE = -3;
+
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 34308dfb3..5edc4314f 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -52,6 +52,9 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
/** The number of contacts in the most recent dictionary rebuild. */
static private int sContactCountAtLastRebuild = 0;
+ /** The locale for this contacts dictionary. Controls name bigram predictions. */
+ public final Locale mLocale;
+
private ContentObserver mObserver;
/**
@@ -59,8 +62,9 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
*/
private final boolean mUseFirstLastBigrams;
- public ContactsBinaryDictionary(final Context context, final int dicTypeId, Locale locale) {
- super(context, getFilenameWithLocale(NAME, locale.toString()), dicTypeId);
+ public ContactsBinaryDictionary(final Context context, Locale locale) {
+ super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS);
+ mLocale = locale;
mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
registerObserver(context);
@@ -116,12 +120,6 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
}
}
- @Override
- public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
- super.getBigrams(codes, previousWord, callback);
- }
-
private boolean useFirstLastBigramsForLocale(Locale locale) {
// TODO: Add firstname/lastname bigram rules for other languages.
if (locale != null && locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
@@ -163,7 +161,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
* bigrams depending on locale.
*/
private void addName(String name) {
- int len = name.codePointCount(0, name.length());
+ int len = StringUtils.codePointCount(name);
String prevWord = null;
// TODO: Better tokenization for non-Latin writing systems
for (int i = 0; i < len; i++) {
@@ -173,7 +171,7 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
i = end - 1;
// Don't add single letter words, possibly confuses
// capitalization of i.
- final int wordLen = word.codePointCount(0, word.length());
+ final int wordLen = StringUtils.codePointCount(word);
if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS);
if (!TextUtils.isEmpty(prevWord)) {
@@ -262,14 +260,14 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
* Checks if the words in a name are in the current binary dictionary.
*/
private boolean isNameInDictionary(String name) {
- int len = name.codePointCount(0, name.length());
+ int len = StringUtils.codePointCount(name);
String prevWord = null;
for (int i = 0; i < len; i++) {
if (Character.isLetter(name.codePointAt(i))) {
int end = getWordEndPosition(name, len, i);
String word = name.substring(i, end);
i = end - 1;
- final int wordLen = word.codePointCount(0, word.length());
+ final int wordLen = StringUtils.codePointCount(word);
if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) {
if (!super.isValidBigramLocked(prevWord, word)) {
diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java
deleted file mode 100644
index cbfbd0ec8..000000000
--- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.os.SystemClock;
-import android.provider.BaseColumns;
-import android.provider.ContactsContract.Contacts;
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.inputmethod.keyboard.Keyboard;
-
-// TODO: This class is superseded by {@link ContactsBinaryDictionary}. Should be cleaned up.
-/**
- * An expandable dictionary that stores the words from Contacts provider.
- *
- * @deprecated Use {@link ContactsBinaryDictionary}.
- */
-@Deprecated
-public class ContactsDictionary extends ExpandableDictionary {
-
- private static final String[] PROJECTION = {
- BaseColumns._ID,
- Contacts.DISPLAY_NAME,
- };
-
- private static final String TAG = "ContactsDictionary";
-
- /**
- * Frequency for contacts information into the dictionary
- */
- private static final int FREQUENCY_FOR_CONTACTS = 40;
- private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
-
- private static final int INDEX_NAME = 1;
-
- private ContentObserver mObserver;
-
- private long mLastLoadedContacts;
-
- public ContactsDictionary(final Context context, final int dicTypeId) {
- super(context, dicTypeId);
- registerObserver(context);
- loadDictionary();
- }
-
- private synchronized void registerObserver(final Context context) {
- // Perform a managed query. The Activity will handle closing and requerying the cursor
- // when needed.
- if (mObserver != null) return;
- ContentResolver cres = context.getContentResolver();
- cres.registerContentObserver(
- Contacts.CONTENT_URI, true, mObserver = new ContentObserver(null) {
- @Override
- public void onChange(boolean self) {
- setRequiresReload(true);
- }
- });
- }
-
- public void reopen(final Context context) {
- registerObserver(context);
- }
-
- @Override
- public synchronized void close() {
- if (mObserver != null) {
- getContext().getContentResolver().unregisterContentObserver(mObserver);
- mObserver = null;
- }
- super.close();
- }
-
- @Override
- public void startDictionaryLoadingTaskLocked() {
- long now = SystemClock.uptimeMillis();
- if (mLastLoadedContacts == 0
- || now - mLastLoadedContacts > 30 * 60 * 1000 /* 30 minutes */) {
- super.startDictionaryLoadingTaskLocked();
- }
- }
-
- @Override
- public void loadDictionaryAsync() {
- try {
- Cursor cursor = getContext().getContentResolver()
- .query(Contacts.CONTENT_URI, PROJECTION, null, null, null);
- if (cursor != null) {
- addWords(cursor);
- }
- } catch(IllegalStateException e) {
- Log.e(TAG, "Contacts DB is having problems");
- }
- mLastLoadedContacts = SystemClock.uptimeMillis();
- }
-
- @Override
- public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
- // Do not return bigrams from Contacts when nothing was typed.
- if (codes.size() <= 0) return;
- super.getBigrams(codes, previousWord, callback);
- }
-
- private void addWords(Cursor cursor) {
- clearDictionary();
-
- final int maxWordLength = getMaxWordLength();
- try {
- if (cursor.moveToFirst()) {
- while (!cursor.isAfterLast()) {
- String name = cursor.getString(INDEX_NAME);
-
- if (name != null && -1 == name.indexOf('@')) {
- int len = name.length();
- String prevWord = null;
-
- // TODO: Better tokenization for non-Latin writing systems
- for (int i = 0; i < len; i++) {
- if (Character.isLetter(name.charAt(i))) {
- int j;
- for (j = i + 1; j < len; j++) {
- char c = name.charAt(j);
-
- if (!(c == Keyboard.CODE_DASH
- || c == Keyboard.CODE_SINGLE_QUOTE
- || Character.isLetter(c))) {
- break;
- }
- }
-
- String word = name.substring(i, j);
- i = j - 1;
-
- // Safeguard against adding really long words. Stack
- // may overflow due to recursion
- // Also don't add single letter words, possibly confuses
- // capitalization of i.
- final int wordLen = word.length();
- if (wordLen < maxWordLength && wordLen > 1) {
- super.addWord(word, null /* shortcut */,
- FREQUENCY_FOR_CONTACTS);
- if (!TextUtils.isEmpty(prevWord)) {
- super.setBigramAndGetFrequency(prevWord, word,
- FREQUENCY_FOR_CONTACTS_BIGRAM);
- }
- prevWord = word;
- }
- }
- }
- }
- cursor.moveToNext();
- }
- }
- cursor.close();
- } catch(IllegalStateException e) {
- Log.e(TAG, "Contacts DB is having problems");
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java
index af7649863..3af3cab2c 100644
--- a/java/src/com/android/inputmethod/latin/DebugSettings.java
+++ b/java/src/com/android/inputmethod/latin/DebugSettings.java
@@ -23,17 +23,20 @@ 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.util.Log;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.research.ResearchLogger;
-public class DebugSettings extends PreferenceFragment
+public final class DebugSettings extends PreferenceFragment
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = DebugSettings.class.getSimpleName();
private static final String DEBUG_MODE_KEY = "debug_mode";
public static final String FORCE_NON_DISTINCT_MULTITOUCH_KEY = "force_non_distinct_multitouch";
+ public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
private boolean mServiceNeedsRestart = false;
private CheckBoxPreference mDebugMode;
@@ -45,6 +48,14 @@ public class DebugSettings extends PreferenceFragment
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
prefs.registerOnSharedPreferenceChangeListener(this);
+ final Preference usabilityStudyPref = findPreference(PREF_USABILITY_STUDY_MODE);
+ if (usabilityStudyPref instanceof CheckBoxPreference) {
+ final CheckBoxPreference checkbox = (CheckBoxPreference)usabilityStudyPref;
+ checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE,
+ ResearchLogger.DEFAULT_USABILITY_STUDY_MODE));
+ checkbox.setSummary(R.string.settings_warning_researcher_mode);
+ }
+
mServiceNeedsRestart = false;
mDebugMode = (CheckBoxPreference) findPreference(DEBUG_MODE_KEY);
updateDebugMode();
diff --git a/java/src/com/android/inputmethod/latin/DebugSettingsActivity.java b/java/src/com/android/inputmethod/latin/DebugSettingsActivity.java
index cde20606a..6ef19ee82 100644
--- a/java/src/com/android/inputmethod/latin/DebugSettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/DebugSettingsActivity.java
@@ -20,7 +20,7 @@ import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceActivity;
-public class DebugSettingsActivity extends PreferenceActivity {
+public final class DebugSettingsActivity extends PreferenceActivity {
@Override
public Intent getIntent() {
final Intent modIntent = new Intent(super.getIntent());
diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
new file mode 100644
index 000000000..ce1b64660
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
@@ -0,0 +1,75 @@
+/*
+ * 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 java.util.Locale;
+
+public final class DicTraverseSession {
+ static {
+ JniUtils.loadNativeLibrary();
+ }
+
+ private native long setDicTraverseSessionNative(String locale);
+ private native void initDicTraverseSessionNative(long nativeDicTraverseSession,
+ long dictionary, int[] previousWord, int previousWordLength);
+ private native void releaseDicTraverseSessionNative(long nativeDicTraverseSession);
+
+ private long mNativeDicTraverseSession;
+
+ public DicTraverseSession(Locale locale, long dictionary) {
+ mNativeDicTraverseSession = createNativeDicTraverseSession(
+ locale != null ? locale.toString() : "");
+ initSession(dictionary);
+ }
+
+ public long getSession() {
+ return mNativeDicTraverseSession;
+ }
+
+ public void initSession(long dictionary) {
+ initSession(dictionary, null, 0);
+ }
+
+ public void initSession(long dictionary, int[] previousWord, int previousWordLength) {
+ initDicTraverseSessionNative(
+ mNativeDicTraverseSession, dictionary, previousWord, previousWordLength);
+ }
+
+ private final long createNativeDicTraverseSession(String locale) {
+ return setDicTraverseSessionNative(locale);
+ }
+
+ private void closeInternal() {
+ if (mNativeDicTraverseSession != 0) {
+ releaseDicTraverseSessionNative(mNativeDicTraverseSession);
+ mNativeDicTraverseSession = 0;
+ }
+ }
+
+ public void close() {
+ closeInternal();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ closeInternal();
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index 7cd9bc2a8..88d0c09dd 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -17,6 +17,9 @@
package com.android.inputmethod.latin;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+
+import java.util.ArrayList;
/**
* Abstract base class for a dictionary that can do a fuzzy search for words based on a set of key
@@ -28,54 +31,41 @@ public abstract class Dictionary {
*/
protected static final int FULL_WORD_SCORE_MULTIPLIER = 2;
- public static final int UNIGRAM = 0;
- public static final int BIGRAM = 1;
-
public static final int NOT_A_PROBABILITY = -1;
- /**
- * Interface to be implemented by classes requesting words to be fetched from the dictionary.
- * @see #getWords(WordComposer, CharSequence, WordCallback, ProximityInfo)
- */
- public interface WordCallback {
- /**
- * Adds a word to a list of suggestions. The word is expected to be ordered based on
- * the provided score.
- * @param word the character array containing the word
- * @param wordOffset starting offset of the word in the character array
- * @param wordLength length of valid characters in the character array
- * @param score the score of occurrence. This is normalized between 1 and 255, but
- * can exceed those limits
- * @param dicTypeId of the dictionary where word was from
- * @param dataType tells type of this data, either UNIGRAM or BIGRAM
- * @return true if the word was added, false if no more words are required
- */
- boolean addWord(char[] word, int wordOffset, int wordLength, int score, int dicTypeId,
- int dataType);
+
+ public static final String TYPE_USER_TYPED = "user_typed";
+ public static final String TYPE_APPLICATION_DEFINED = "application_defined";
+ public static final String TYPE_HARDCODED = "hardcoded"; // punctuation signs and such
+ 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.
+ public static final String TYPE_USER_HISTORY = "history";
+ protected final String mDictType;
+
+ public Dictionary(final String dictType) {
+ mDictType = dictType;
}
/**
- * Searches for words in the dictionary that match the characters in the composer. Matched
- * words are added through the callback object.
- * @param composer the key sequence to match
- * @param prevWordForBigrams the previous word, or null if none
- * @param callback the callback object to send matched words to as possible candidates
+ * Searches for suggestions for a given context. For the moment the context is only the
+ * previous word.
+ * @param composer the key sequence to match with coordinate info, as a WordComposer
+ * @param prevWord the previous word, or null if none
* @param proximityInfo the object for key proximity. May be ignored by some implementations.
- * @see WordCallback#addWord(char[], int, int, int, int, int)
+ * @return the list of suggestions (possibly null if none)
*/
- abstract public void getWords(final WordComposer composer,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo);
+ // 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 CharSequence prevWord, final ProximityInfo proximityInfo);
- /**
- * Searches for pairs in the bigram dictionary that matches the previous word and all the
- * possible words following are added through the callback object.
- * @param composer the key sequence to match
- * @param previousWord the word before
- * @param callback the callback object to send possible word following previous word
- */
- public void getBigrams(final WordComposer composer, final CharSequence previousWord,
- final WordCallback callback) {
- // empty base implementation
+ // 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 CharSequence prevWord, final ProximityInfo proximityInfo, int sessionId) {
+ return getSuggestions(composer, prevWord, proximityInfo);
}
/**
@@ -115,4 +105,12 @@ public abstract class Dictionary {
public void close() {
// empty base implementation
}
+
+ /**
+ * Subclasses may override to indicate that this Dictionary is not yet properly initialized.
+ */
+
+ public boolean isInitialized() {
+ return true;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index 1a05fcd86..d3b120989 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -17,9 +17,11 @@
package com.android.inputmethod.latin;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import android.util.Log;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -27,40 +29,48 @@ import java.util.concurrent.CopyOnWriteArrayList;
/**
* Class for a collection of dictionaries that behave like one dictionary.
*/
-public class DictionaryCollection extends Dictionary {
+public final class DictionaryCollection extends Dictionary {
private final String TAG = DictionaryCollection.class.getSimpleName();
protected final CopyOnWriteArrayList<Dictionary> mDictionaries;
- public DictionaryCollection() {
- mDictionaries = new CopyOnWriteArrayList<Dictionary>();
+ public DictionaryCollection(final String dictType) {
+ super(dictType);
+ mDictionaries = CollectionUtils.newCopyOnWriteArrayList();
}
- public DictionaryCollection(Dictionary... dictionaries) {
+ public DictionaryCollection(final String dictType, Dictionary... dictionaries) {
+ super(dictType);
if (null == dictionaries) {
- mDictionaries = new CopyOnWriteArrayList<Dictionary>();
+ mDictionaries = CollectionUtils.newCopyOnWriteArrayList();
} else {
- mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+ mDictionaries = CollectionUtils.newCopyOnWriteArrayList(dictionaries);
mDictionaries.removeAll(Collections.singleton(null));
}
}
- public DictionaryCollection(Collection<Dictionary> dictionaries) {
- mDictionaries = new CopyOnWriteArrayList<Dictionary>(dictionaries);
+ public DictionaryCollection(final String dictType, Collection<Dictionary> dictionaries) {
+ super(dictType);
+ mDictionaries = CollectionUtils.newCopyOnWriteArrayList(dictionaries);
mDictionaries.removeAll(Collections.singleton(null));
}
@Override
- public void getWords(final WordComposer composer, final CharSequence prevWordForBigrams,
- final WordCallback callback, final ProximityInfo proximityInfo) {
- for (final Dictionary dict : mDictionaries)
- dict.getWords(composer, prevWordForBigrams, callback, proximityInfo);
- }
-
- @Override
- public void getBigrams(final WordComposer composer, final CharSequence previousWord,
- final WordCallback callback) {
- for (final Dictionary dict : mDictionaries)
- dict.getBigrams(composer, previousWord, callback);
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo) {
+ 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);
+ 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);
+ if (null != sugg) suggestions.addAll(sugg);
+ }
+ return suggestions;
}
@Override
@@ -82,8 +92,9 @@ public class DictionaryCollection extends Dictionary {
return maxFreq;
}
- public boolean isEmpty() {
- return mDictionaries.isEmpty();
+ @Override
+ public boolean isInitialized() {
+ return !mDictionaries.isEmpty();
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index a22d73af7..f381973ae 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -29,7 +29,7 @@ import java.util.Locale;
/**
* Factory for dictionary instances.
*/
-public class DictionaryFactory {
+public final class DictionaryFactory {
private static final String TAG = DictionaryFactory.class.getSimpleName();
// This class must be located in the same package as LatinIME.java.
private static final String RESOURCE_PACKAGE_NAME =
@@ -49,17 +49,18 @@ public class DictionaryFactory {
final Locale locale, final boolean useFullEditDistance) {
if (null == locale) {
Log.e(TAG, "No locale defined for dictionary");
- return new DictionaryCollection(createBinaryDictionary(context, locale));
+ return new DictionaryCollection(Dictionary.TYPE_MAIN,
+ createBinaryDictionary(context, locale));
}
- final LinkedList<Dictionary> dictList = new LinkedList<Dictionary>();
+ final LinkedList<Dictionary> dictList = CollectionUtils.newLinkedList();
final ArrayList<AssetFileAddress> assetFileList =
BinaryDictionaryGetter.getDictionaryFiles(locale, context);
if (null != assetFileList) {
for (final AssetFileAddress f : assetFileList) {
final BinaryDictionary binaryDictionary =
new BinaryDictionary(context, f.mFilename, f.mOffset, f.mLength,
- useFullEditDistance, locale);
+ useFullEditDistance, locale, Dictionary.TYPE_MAIN);
if (binaryDictionary.isValidDictionary()) {
dictList.add(binaryDictionary);
}
@@ -69,7 +70,7 @@ public class DictionaryFactory {
// If the list is empty, that means we should not use any dictionary (for example, the user
// explicitly disabled the main dictionary), so the following is okay. dictList is never
// null, but if for some reason it is, DictionaryCollection handles it gracefully.
- return new DictionaryCollection(dictList);
+ return new DictionaryCollection(Dictionary.TYPE_MAIN, dictList);
}
/**
@@ -112,7 +113,7 @@ public class DictionaryFactory {
return null;
}
return new BinaryDictionary(context, sourceDir, afd.getStartOffset(), afd.getLength(),
- false /* useFullEditDistance */, locale);
+ false /* useFullEditDistance */, locale, Dictionary.TYPE_MAIN);
} catch (android.content.res.Resources.NotFoundException e) {
Log.e(TAG, "Could not find the resource");
return null;
@@ -140,7 +141,7 @@ public class DictionaryFactory {
long startOffset, long length, final boolean useFullEditDistance, Locale locale) {
if (dictionary.isFile()) {
return new BinaryDictionary(context, dictionary.getAbsolutePath(), startOffset, length,
- useFullEditDistance, locale);
+ useFullEditDistance, locale, Dictionary.TYPE_MAIN);
} else {
Log.e(TAG, "Could not find the file. path=" + dictionary.getAbsolutePath());
return null;
diff --git a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
index 9c37d7673..f2f3fbded 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
@@ -27,7 +27,7 @@ import android.net.Uri;
/**
* Takes action to reload the necessary data when a dictionary pack was added/removed.
*/
-public class DictionaryPackInstallBroadcastReceiver extends BroadcastReceiver {
+public final class DictionaryPackInstallBroadcastReceiver extends BroadcastReceiver {
final LatinIME mService;
/**
diff --git a/java/src/com/android/inputmethod/latin/EditingUtils.java b/java/src/com/android/inputmethod/latin/EditingUtils.java
deleted file mode 100644
index 0f34d50bb..000000000
--- a/java/src/com/android/inputmethod/latin/EditingUtils.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.latin;
-
-import android.view.inputmethod.ExtractedText;
-import android.view.inputmethod.ExtractedTextRequest;
-import android.view.inputmethod.InputConnection;
-
-import java.util.regex.Pattern;
-
-/**
- * Utility methods to deal with editing text through an InputConnection.
- */
-public class EditingUtils {
- /**
- * Number of characters we want to look back in order to identify the previous word
- */
- // Provision for a long word pair and a separator
- private static final int LOOKBACK_CHARACTER_NUM = BinaryDictionary.MAX_WORD_LENGTH * 2 + 1;
- private static final int INVALID_CURSOR_POSITION = -1;
-
- private EditingUtils() {
- // Unintentional empty constructor for singleton.
- }
-
- private static int getCursorPosition(InputConnection connection) {
- if (null == connection) return INVALID_CURSOR_POSITION;
- final ExtractedText extracted = connection.getExtractedText(new ExtractedTextRequest(), 0);
- if (extracted == null) {
- return INVALID_CURSOR_POSITION;
- }
- return extracted.startOffset + extracted.selectionStart;
- }
-
- /**
- * @param connection connection to the current text field.
- * @param separators characters which may separate words
- * @return the word that surrounds the cursor, including up to one trailing
- * separator. For example, if the field contains "he|llo world", where |
- * represents the cursor, then "hello " will be returned.
- */
- public static String getWordAtCursor(InputConnection connection, String separators) {
- // getWordRangeAtCursor returns null if the connection is null
- Range r = getWordRangeAtCursor(connection, separators);
- return (r == null) ? null : r.mWord;
- }
-
- /**
- * Represents a range of text, relative to the current cursor position.
- */
- public static 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 String mWord;
-
- public Range(int charsBefore, int charsAfter, String word) {
- if (charsBefore < 0 || charsAfter < 0) {
- throw new IndexOutOfBoundsException();
- }
- this.mCharsBefore = charsBefore;
- this.mCharsAfter = charsAfter;
- this.mWord = word;
- }
- }
-
- private static Range getWordRangeAtCursor(InputConnection connection, String sep) {
- if (connection == null || sep == null) {
- return null;
- }
- CharSequence before = connection.getTextBeforeCursor(1000, 0);
- CharSequence after = connection.getTextAfterCursor(1000, 0);
- if (before == null || after == null) {
- return null;
- }
-
- // Find first word separator before the cursor
- int start = before.length();
- while (start > 0 && !isWhitespace(before.charAt(start - 1), sep)) start--;
-
- // Find last word separator after the cursor
- int end = -1;
- while (++end < after.length() && !isWhitespace(after.charAt(end), sep)) {
- // Nothing to do here.
- }
-
- int cursor = getCursorPosition(connection);
- if (start >= 0 && cursor + end <= after.length() + before.length()) {
- String word = before.toString().substring(start, before.length())
- + after.toString().substring(0, end);
- return new Range(before.length() - start, end, word);
- }
-
- return null;
- }
-
- private static boolean isWhitespace(int code, String whitespace) {
- return whitespace.contains(String.valueOf((char) code));
- }
-
- private static final Pattern spaceRegex = Pattern.compile("\\s+");
-
- public static CharSequence getPreviousWord(InputConnection connection,
- String sentenceSeperators) {
- //TODO: Should fix this. This could be slow!
- if (null == connection) return null;
- CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
- return getPreviousWord(prev, sentenceSeperators);
- }
-
- // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
- // Also, it won't return words that end in a separator.
- // Example :
- // "abc def|" -> abc
- // "abc def |" -> abc
- // "abc def. |" -> abc
- // "abc def . |" -> def
- // "abc|" -> null
- // "abc |" -> null
- // "abc. def|" -> null
- public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
- if (prev == null) return null;
- String[] w = spaceRegex.split(prev);
-
- // If we can't find two words, or we found an empty word, return null.
- if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
-
- // If ends in a separator, return null
- char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
- if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
-
- return w[w.length - 2];
- }
-
- public static CharSequence getThisWord(InputConnection connection, String sentenceSeperators) {
- if (null == connection) return null;
- final CharSequence prev = connection.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
- return getThisWord(prev, sentenceSeperators);
- }
-
- // Get the word immediately before the cursor, even if there is whitespace between it and
- // the cursor - but not if there is punctuation.
- // Example :
- // "abc def|" -> def
- // "abc def |" -> def
- // "abc def. |" -> null
- // "abc def . |" -> null
- public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
- if (prev == null) return null;
- String[] w = spaceRegex.split(prev);
-
- // No word : return null
- if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
-
- // If ends in a separator, return null
- char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
- if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
-
- return w[w.length - 1];
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index c65404cbc..b93c17f11 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -19,7 +19,9 @@ import android.os.SystemClock;
import android.util.Log;
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;
@@ -61,7 +63,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* that filename.
*/
private static final HashMap<String, DictionaryController> sSharedDictionaryControllers =
- new HashMap<String, DictionaryController>();
+ CollectionUtils.newHashMap();
/** The application context. */
protected final Context mContext;
@@ -75,9 +77,6 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
/** The expandable fusion dictionary used to generate the binary dictionary. */
private FusionDictionary mFusionDictionary;
- /** The dictionary type id. */
- public final int mDicTypeId;
-
/**
* 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
@@ -91,6 +90,10 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
/** Controls access to the local binary dictionary for this instance. */
private final DictionaryController mLocalDictionaryController = new DictionaryController();
+ private static final int BINARY_DICT_VERSION = 1;
+ private static final FormatSpec.FormatOptions FORMAT_OPTIONS =
+ new FormatSpec.FormatOptions(BINARY_DICT_VERSION);
+
/**
* Abstract method for loading the unigrams and bigrams of a given dictionary in a background
* thread.
@@ -123,11 +126,11 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* @param context The application context of the parent.
* @param filename The filename for this binary dictionary. Multiple dictionaries with the same
* filename is supported.
- * @param dictType The type of this dictionary.
+ * @param dictType the dictionary type, as a human-readable string
*/
public ExpandableBinaryDictionary(
- final Context context, final String filename, final int dictType) {
- mDicTypeId = dictType;
+ final Context context, final String filename, final String dictType) {
+ super(dictType);
mFilename = filename;
mContext = context;
mBinaryDictionary = null;
@@ -161,9 +164,9 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
* the native side.
*/
public void clearFusionDictionary() {
+ final HashMap<String, String> attributes = CollectionUtils.newHashMap();
mFusionDictionary = new FusionDictionary(new Node(),
- new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false,
- false));
+ new FusionDictionary.DictionaryOptions(attributes, false, false));
}
/**
@@ -174,12 +177,12 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
// considering performance regression.
protected void addWord(final String word, final String shortcutTarget, final int frequency) {
if (shortcutTarget == null) {
- mFusionDictionary.add(word, frequency, null);
+ mFusionDictionary.add(word, frequency, null, false /* isNotAWord */);
} else {
// TODO: Do this in the subclass, with this class taking an arraylist.
- final ArrayList<WeightedString> shortcutTargets = new ArrayList<WeightedString>();
+ final ArrayList<WeightedString> shortcutTargets = CollectionUtils.newArrayList();
shortcutTargets.add(new WeightedString(shortcutTarget, frequency));
- mFusionDictionary.add(word, frequency, shortcutTargets);
+ mFusionDictionary.add(word, frequency, shortcutTargets, false /* isNotAWord */);
}
}
@@ -194,46 +197,19 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
}
@Override
- public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
- final WordCallback callback, final ProximityInfo proximityInfo) {
- asyncReloadDictionaryIfRequired();
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
- }
-
- protected final void getWordsInner(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
- // Ensure that there are no concurrent calls to getWords. If there are, do nothing and
- // return.
- if (mLocalDictionaryController.tryLock()) {
- try {
- if (mBinaryDictionary != null) {
- mBinaryDictionary.getWords(codes, prevWordForBigrams, callback, proximityInfo);
- }
- } finally {
- mLocalDictionaryController.unlock();
- }
- }
- }
-
- @Override
- public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo) {
asyncReloadDictionaryIfRequired();
- getBigramsInner(codes, previousWord, callback);
- }
-
- protected void getBigramsInner(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
if (mLocalDictionaryController.tryLock()) {
try {
if (mBinaryDictionary != null) {
- mBinaryDictionary.getBigrams(codes, previousWord, callback);
+ return mBinaryDictionary.getSuggestions(composer, prevWord, proximityInfo);
}
} finally {
mLocalDictionaryController.unlock();
}
}
+ return null;
}
@Override
@@ -306,7 +282,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
// Build the new binary dictionary
final BinaryDictionary newBinaryDictionary =
new BinaryDictionary(mContext, filename, 0, length, true /* useFullEditDistance */,
- null);
+ null, mDictType);
if (mBinaryDictionary != null) {
// Ensure all threads accessing the current dictionary have finished before swapping in
@@ -339,7 +315,7 @@ abstract public class ExpandableBinaryDictionary extends Dictionary {
FileOutputStream out = null;
try {
out = new FileOutputStream(tempFile);
- BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, 1);
+ BinaryDictInputOutput.writeDictionaryBinary(out, mFusionDictionary, FORMAT_OPTIONS);
out.flush();
out.close();
tempFile.renameTo(file);
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index 34a92fd30..8cdc2a0af 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -17,10 +17,11 @@
package com.android.inputmethod.latin;
import android.content.Context;
+import android.text.TextUtils;
-import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.UserHistoryForgettingCurveUtils.ForgettingCurveParams;
import java.util.ArrayList;
@@ -37,7 +38,6 @@ public class ExpandableDictionary extends Dictionary {
private Context mContext;
private char[] mWordBuilder = new char[BinaryDictionary.MAX_WORD_LENGTH];
- private int mDicTypeId;
private int mMaxDepth;
private int mInputLength;
@@ -48,7 +48,7 @@ public class ExpandableDictionary extends Dictionary {
// Use this lock before touching mUpdatingDictionary & mRequiresDownload
private Object mUpdatingLock = new Object();
- private static class Node {
+ private static final class Node {
Node() {}
char mCode;
int mFrequency;
@@ -60,7 +60,7 @@ public class ExpandableDictionary extends Dictionary {
LinkedList<NextWord> mNGrams; // Supports ngram
}
- private static class NodeArray {
+ private static final class NodeArray {
Node[] mData;
int mLength = 0;
private static final int INCREMENT = 2;
@@ -88,7 +88,7 @@ public class ExpandableDictionary extends Dictionary {
public int notifyTypedAgainAndGetFrequency();
}
- private static class NextStaticWord implements NextWord {
+ private static final class NextStaticWord implements NextWord {
public final Node mWord;
private final int mFrequency;
public NextStaticWord(Node word, int frequency) {
@@ -117,7 +117,7 @@ public class ExpandableDictionary extends Dictionary {
}
}
- private static class NextHistoryWord implements NextWord {
+ private static final class NextHistoryWord implements NextWord {
public final Node mWord;
public final ForgettingCurveParams mFcp;
@@ -151,11 +151,11 @@ public class ExpandableDictionary extends Dictionary {
private int[][] mCodes;
- public ExpandableDictionary(Context context, int dicTypeId) {
+ public ExpandableDictionary(final Context context, final String dictType) {
+ super(dictType);
mContext = context;
clearDictionary();
mCodes = new int[BinaryDictionary.MAX_WORD_LENGTH][];
- mDicTypeId = dicTypeId;
}
public void loadDictionary() {
@@ -230,7 +230,7 @@ public class ExpandableDictionary extends Dictionary {
childNode.mTerminal = true;
if (isShortcutOnly) {
if (null == childNode.mShortcutTargets) {
- childNode.mShortcutTargets = new ArrayList<char[]>();
+ childNode.mShortcutTargets = CollectionUtils.newArrayList();
}
childNode.mShortcutTargets.add(shortcutTarget.toCharArray());
} else {
@@ -247,27 +247,43 @@ public class ExpandableDictionary extends Dictionary {
}
@Override
- public void getWords(final WordComposer codes, final CharSequence prevWordForBigrams,
- final WordCallback callback, final ProximityInfo proximityInfo) {
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo) {
+ if (reloadDictionaryIfRequired()) return null;
+ if (composer.size() > 1) {
+ if (composer.size() >= BinaryDictionary.MAX_WORD_LENGTH) {
+ return null;
+ }
+ final ArrayList<SuggestedWordInfo> suggestions =
+ getWordsInner(composer, prevWord, proximityInfo);
+ return suggestions;
+ } else {
+ if (TextUtils.isEmpty(prevWord)) return null;
+ final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
+ runBigramReverseLookUp(prevWord, suggestions);
+ return suggestions;
+ }
+ }
+
+ // This reloads the dictionary if required, and returns whether it's currently updating its
+ // contents or not.
+ // @VisibleForTesting
+ boolean reloadDictionaryIfRequired() {
synchronized (mUpdatingLock) {
// If we need to update, start off a background task
if (mRequiresReload) startDictionaryLoadingTaskLocked();
- // Currently updating contacts, don't return any results.
- if (mUpdatingDictionary) return;
- }
- if (codes.size() >= BinaryDictionary.MAX_WORD_LENGTH) {
- return;
+ return mUpdatingDictionary;
}
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
}
- protected final void getWordsInner(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
+ protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer codes,
+ final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) {
+ final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
mInputLength = codes.size();
if (mCodes.length < mInputLength) mCodes = new int[mInputLength][];
- final int[] xCoordinates = codes.getXCoordinates();
- final int[] yCoordinates = codes.getYCoordinates();
+ final InputPointers ips = codes.getInputPointers();
+ final int[] xCoordinates = ips.getXCoordinates();
+ final int[] yCoordinates = ips.getYCoordinates();
// Cache the codes so that we don't have to lookup an array list
for (int i = 0; i < mInputLength; i++) {
// TODO: Calculate proximity info here.
@@ -275,16 +291,17 @@ public class ExpandableDictionary extends Dictionary {
mCodes[i] = new int[ProximityInfo.MAX_PROXIMITY_CHARS_SIZE];
}
final int x = xCoordinates != null && i < xCoordinates.length ?
- xCoordinates[i] : WordComposer.NOT_A_COORDINATE;
+ xCoordinates[i] : Constants.NOT_A_COORDINATE;
final int y = xCoordinates != null && i < yCoordinates.length ?
- yCoordinates[i] : WordComposer.NOT_A_COORDINATE;
+ yCoordinates[i] : Constants.NOT_A_COORDINATE;
proximityInfo.fillArrayWithNearestKeyCodes(x, y, codes.getCodeAt(i), mCodes[i]);
}
mMaxDepth = mInputLength * 3;
- getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, callback);
+ getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, -1, suggestions);
for (int i = 0; i < mInputLength; i++) {
- getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, callback);
+ getWordsRec(mRoots, codes, mWordBuilder, 0, false, 1, 0, i, suggestions);
}
+ return suggestions;
}
@Override
@@ -368,24 +385,27 @@ public class ExpandableDictionary extends Dictionary {
* @param word the word to insert, as an array of code points
* @param depth the depth of the node in the tree
* @param finalFreq the frequency for this word
+ * @param suggestions the suggestion collection to add the suggestions to
* @return whether there is still space for more words.
- * @see Dictionary.WordCallback#addWord(char[], int, int, int, int, int)
*/
private boolean addWordAndShortcutsFromNode(final Node node, final char[] word, final int depth,
- final int finalFreq, final WordCallback callback) {
+ final int finalFreq, final ArrayList<SuggestedWordInfo> suggestions) {
if (finalFreq > 0 && !node.mShortcutOnly) {
- if (!callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId, Dictionary.UNIGRAM)) {
- return false;
- }
+ // Use KIND_CORRECTION always. This dictionary does not really have a notion of
+ // COMPLETION against CORRECTION; we could artificially add one by looking at
+ // the respective size of the typed word and the suggestion if it matters sometime
+ // in the future.
+ suggestions.add(new SuggestedWordInfo(new String(word, 0, depth + 1), finalFreq,
+ SuggestedWordInfo.KIND_CORRECTION, mDictType));
+ if (suggestions.size() >= Suggest.MAX_SUGGESTIONS) return false;
}
if (null != node.mShortcutTargets) {
final int length = node.mShortcutTargets.size();
for (int shortcutIndex = 0; shortcutIndex < length; ++shortcutIndex) {
final char[] shortcut = node.mShortcutTargets.get(shortcutIndex);
- if (!callback.addWord(shortcut, 0, shortcut.length, finalFreq, mDicTypeId,
- Dictionary.UNIGRAM)) {
- return false;
- }
+ suggestions.add(new SuggestedWordInfo(new String(shortcut, 0, shortcut.length),
+ finalFreq, SuggestedWordInfo.KIND_SHORTCUT, mDictType));
+ if (suggestions.size() > Suggest.MAX_SUGGESTIONS) return false;
}
}
return true;
@@ -408,12 +428,12 @@ public class ExpandableDictionary extends Dictionary {
* case we skip over some punctuations such as apostrophe in the traversal. That is, if you type
* "wouldve", it could be matching "would've", so the depth will be one more than the
* inputIndex
- * @param callback the callback class for adding a word
+ * @param suggestions the list in which to add suggestions
*/
// TODO: Share this routine with the native code for BinaryDictionary
protected void getWordsRec(NodeArray roots, final WordComposer codes, final char[] word,
final int depth, final boolean completion, int snr, int inputIndex, int skipPos,
- WordCallback callback) {
+ final ArrayList<SuggestedWordInfo> suggestions) {
final int count = roots.mLength;
final int codeSize = mInputLength;
// Optimization: Prune out words that are too long compared to how much was typed.
@@ -443,14 +463,14 @@ public class ExpandableDictionary extends Dictionary {
} else {
finalFreq = computeSkippedWordFinalFreq(freq, snr, mInputLength);
}
- if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, callback)) {
+ if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, suggestions)) {
// No space left in the queue, bail out
return;
}
}
if (children != null) {
getWordsRec(children, codes, word, depth + 1, true, snr, inputIndex,
- skipPos, callback);
+ skipPos, suggestions);
}
} else if ((c == Keyboard.CODE_SINGLE_QUOTE
&& currentChars[0] != Keyboard.CODE_SINGLE_QUOTE) || depth == skipPos) {
@@ -458,7 +478,7 @@ public class ExpandableDictionary extends Dictionary {
word[depth] = c;
if (children != null) {
getWordsRec(children, codes, word, depth + 1, completion, snr, inputIndex,
- skipPos, callback);
+ skipPos, suggestions);
}
} else {
// Don't use alternatives if we're looking for missing characters
@@ -466,7 +486,7 @@ public class ExpandableDictionary extends Dictionary {
for (int j = 0; j < alternativesSize; j++) {
final int addedAttenuation = (j > 0 ? 1 : 2);
final int currentChar = currentChars[j];
- if (currentChar == KeyDetector.NOT_A_CODE) {
+ if (currentChar == Constants.NOT_A_CODE) {
break;
}
if (currentChar == lowerC || currentChar == c) {
@@ -483,7 +503,7 @@ public class ExpandableDictionary extends Dictionary {
snr * addedAttenuation, mInputLength);
}
if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq,
- callback)) {
+ suggestions)) {
// No space left in the queue, bail out
return;
}
@@ -491,12 +511,12 @@ public class ExpandableDictionary extends Dictionary {
if (children != null) {
getWordsRec(children, codes, word, depth + 1,
true, snr * addedAttenuation, inputIndex + 1,
- skipPos, callback);
+ skipPos, suggestions);
}
} else if (children != null) {
getWordsRec(children, codes, word, depth + 1,
false, snr * addedAttenuation, inputIndex + 1,
- skipPos, callback);
+ skipPos, suggestions);
}
}
}
@@ -514,8 +534,10 @@ public class ExpandableDictionary extends Dictionary {
/**
* 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 frequency frequency for this bigram
- * @param addFrequency if true, it adds to current frequency, else it overwrites the old value
+ * @param fcp an instance of ForgettingCurveParams to use for decay policy
* @return returns the final bigram frequency
*/
private int setBigramAndGetFrequency(
@@ -528,7 +550,7 @@ public class ExpandableDictionary extends Dictionary {
Node secondWord = searchWord(mRoots, word2, 0, null);
LinkedList<NextWord> bigrams = firstWord.mNGrams;
if (bigrams == null || bigrams.size() == 0) {
- firstWord.mNGrams = new LinkedList<NextWord>();
+ firstWord.mNGrams = CollectionUtils.newLinkedList();
bigrams = firstWord.mNGrams;
} else {
for (NextWord nw : bigrams) {
@@ -580,32 +602,14 @@ public class ExpandableDictionary extends Dictionary {
return searchWord(childNode.mChildren, word, depth + 1, childNode);
}
- // @VisibleForTesting
- boolean reloadDictionaryIfRequired() {
- synchronized (mUpdatingLock) {
- // If we need to update, start off a background task
- if (mRequiresReload) startDictionaryLoadingTaskLocked();
- // Currently updating contacts, don't return any results.
- return mUpdatingDictionary;
- }
- }
-
private void runBigramReverseLookUp(final CharSequence previousWord,
- final WordCallback callback) {
+ final ArrayList<SuggestedWordInfo> suggestions) {
// Search for the lowercase version of the word only, because that's where bigrams
// store their sons.
Node prevWord = searchNode(mRoots, previousWord.toString().toLowerCase(), 0,
previousWord.length());
if (prevWord != null && prevWord.mNGrams != null) {
- reverseLookUp(prevWord.mNGrams, callback);
- }
- }
-
- @Override
- public void getBigrams(final WordComposer codes, final CharSequence previousWord,
- final WordCallback callback) {
- if (!reloadDictionaryIfRequired()) {
- runBigramReverseLookUp(previousWord, callback);
+ reverseLookUp(prevWord.mNGrams, suggestions);
}
}
@@ -633,11 +637,12 @@ public class ExpandableDictionary extends Dictionary {
/**
* reverseLookUp retrieves the full word given a list of terminal nodes and adds those words
- * through callback.
+ * to the suggestions list passed as an argument.
* @param terminalNodes list of terminal nodes we want to add
+ * @param suggestions the suggestion collection to add the word to
*/
private void reverseLookUp(LinkedList<NextWord> terminalNodes,
- final WordCallback callback) {
+ final ArrayList<SuggestedWordInfo> suggestions) {
Node node;
int freq;
for (NextWord nextWord : terminalNodes) {
@@ -648,11 +653,15 @@ public class ExpandableDictionary extends Dictionary {
--index;
mLookedUpString[index] = node.mCode;
node = node.mParent;
- } while (node != null);
-
- if (freq >= 0) {
- callback.addWord(mLookedUpString, index, BinaryDictionary.MAX_WORD_LENGTH - index,
- freq, mDicTypeId, Dictionary.BIGRAM);
+ } while (node != null && index > 0);
+
+ // If node is null, we have a word longer than MAX_WORD_LENGTH in the dictionary.
+ // It's a little unclear how this can happen, but just in case it does it's safer
+ // to ignore the word in this case.
+ if (freq >= 0 && node == null) {
+ suggestions.add(new SuggestedWordInfo(new String(mLookedUpString, index,
+ BinaryDictionary.MAX_WORD_LENGTH - index),
+ freq, SuggestedWordInfo.KIND_CORRECTION, mDictType));
}
}
}
@@ -694,7 +703,7 @@ public class ExpandableDictionary extends Dictionary {
mRoots = new NodeArray();
}
- private class LoadDictionaryTask extends Thread {
+ private final class LoadDictionaryTask extends Thread {
LoadDictionaryTask() {}
@Override
public void run() {
diff --git a/java/src/com/android/inputmethod/latin/FileTransforms.java b/java/src/com/android/inputmethod/latin/FileTransforms.java
index 80159521c..09cf23a9b 100644
--- a/java/src/com/android/inputmethod/latin/FileTransforms.java
+++ b/java/src/com/android/inputmethod/latin/FileTransforms.java
@@ -21,7 +21,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.GZIPInputStream;
-public class FileTransforms {
+public final class FileTransforms {
public static OutputStream getCryptedStream(OutputStream out) {
// Crypt the stream.
return out;
diff --git a/java/src/com/android/inputmethod/latin/ImfUtils.java b/java/src/com/android/inputmethod/latin/ImfUtils.java
index b882a4860..2674e4575 100644
--- a/java/src/com/android/inputmethod/latin/ImfUtils.java
+++ b/java/src/com/android/inputmethod/latin/ImfUtils.java
@@ -29,7 +29,7 @@ import java.util.List;
/**
* Utility class for Input Method Framework
*/
-public class ImfUtils {
+public final class ImfUtils {
private ImfUtils() {
// This utility class is not publicly instantiable.
}
@@ -90,6 +90,13 @@ public class ImfUtils {
return false;
}
+ public static InputMethodSubtype getCurrentInputMethodSubtype(Context context,
+ InputMethodSubtype defaultSubtype) {
+ final InputMethodManager imm = getInputMethodManager(context);
+ final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
+ return (currentSubtype != null) ? currentSubtype : defaultSubtype;
+ }
+
public static boolean hasMultipleEnabledIMEsOrSubtypes(Context context,
final boolean shouldIncludeAuxiliarySubtypes) {
final InputMethodManager imm = getInputMethodManager(context);
diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java
index 229ae2f3c..2f7608a03 100644
--- a/java/src/com/android/inputmethod/latin/InputAttributes.java
+++ b/java/src/com/android/inputmethod/latin/InputAttributes.java
@@ -23,17 +23,18 @@ import android.view.inputmethod.EditorInfo;
/**
* Class to hold attributes of the input field.
*/
-public class InputAttributes {
+public final class InputAttributes {
private final String TAG = InputAttributes.class.getSimpleName();
final public boolean mInputTypeNoAutoCorrect;
final public boolean mIsSettingsSuggestionStripOn;
final public boolean mApplicationSpecifiedCompletionOn;
- final public int mEditorAction;
+ final private int mInputType;
public InputAttributes(final EditorInfo editorInfo, final boolean isFullscreenMode) {
final int inputType = null != editorInfo ? editorInfo.inputType : 0;
final int inputClass = inputType & InputType.TYPE_MASK_CLASS;
+ mInputType = inputType;
if (inputClass != InputType.TYPE_CLASS_TEXT) {
// If we are not looking at a TYPE_CLASS_TEXT field, the following strange
// cases may arise, so we do a couple sanity checks for them. If it's a
@@ -64,7 +65,7 @@ public class InputAttributes {
final boolean flagAutoComplete =
0 != (inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE);
- // Make sure that passwords are not displayed in {@link SuggestionsView}.
+ // Make sure that passwords are not displayed in {@link SuggestionStripView}.
if (InputTypeUtils.isPasswordInputType(inputType)
|| InputTypeUtils.isVisiblePasswordInputType(inputType)
|| InputTypeUtils.isEmailVariation(variation)
@@ -92,8 +93,10 @@ public class InputAttributes {
mApplicationSpecifiedCompletionOn = flagAutoComplete && isFullscreenMode;
}
- mEditorAction = (editorInfo == null) ? EditorInfo.IME_ACTION_UNSPECIFIED
- : editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
+ }
+
+ public boolean isSameInputType(final EditorInfo editorInfo) {
+ return editorInfo.inputType == mInputType;
}
@SuppressWarnings("unused")
diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java
new file mode 100644
index 000000000..6b48aabb3
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/InputPointers.java
@@ -0,0 +1,133 @@
+/*
+ * 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;
+
+// TODO: This class is not thread-safe.
+public final class InputPointers {
+ private final int mDefaultCapacity;
+ private final ResizableIntArray mXCoordinates;
+ private final ResizableIntArray mYCoordinates;
+ private final ResizableIntArray mPointerIds;
+ private final ResizableIntArray mTimes;
+
+ public InputPointers(int defaultCapacity) {
+ mDefaultCapacity = defaultCapacity;
+ mXCoordinates = new ResizableIntArray(defaultCapacity);
+ mYCoordinates = new ResizableIntArray(defaultCapacity);
+ mPointerIds = new ResizableIntArray(defaultCapacity);
+ mTimes = new ResizableIntArray(defaultCapacity);
+ }
+
+ public void addPointer(int index, int x, int y, int pointerId, int time) {
+ mXCoordinates.add(index, x);
+ mYCoordinates.add(index, y);
+ mPointerIds.add(index, pointerId);
+ mTimes.add(index, time);
+ }
+
+ public void addPointer(int x, int y, int pointerId, int time) {
+ mXCoordinates.add(x);
+ mYCoordinates.add(y);
+ mPointerIds.add(pointerId);
+ mTimes.add(time);
+ }
+
+ public void set(InputPointers ip) {
+ mXCoordinates.set(ip.mXCoordinates);
+ mYCoordinates.set(ip.mYCoordinates);
+ mPointerIds.set(ip.mPointerIds);
+ mTimes.set(ip.mTimes);
+ }
+
+ public void copy(InputPointers ip) {
+ mXCoordinates.copy(ip.mXCoordinates);
+ mYCoordinates.copy(ip.mYCoordinates);
+ mPointerIds.copy(ip.mPointerIds);
+ mTimes.copy(ip.mTimes);
+ }
+
+ /**
+ * Append the pointers in the specified {@link InputPointers} to the end of this.
+ * @param src the source {@link InputPointers} to read the data from.
+ * @param startPos the starting index of the pointers in {@code src}.
+ * @param length the number of pointers to be appended.
+ */
+ public void append(InputPointers src, int startPos, int length) {
+ if (length == 0) {
+ return;
+ }
+ mXCoordinates.append(src.mXCoordinates, startPos, length);
+ mYCoordinates.append(src.mYCoordinates, startPos, length);
+ mPointerIds.append(src.mPointerIds, startPos, length);
+ mTimes.append(src.mTimes, startPos, length);
+ }
+
+ /**
+ * Append the times, x-coordinates and y-coordinates in the specified {@link ResizableIntArray}
+ * to the end of this.
+ * @param pointerId the pointer id of the source.
+ * @param times the source {@link ResizableIntArray} to read the event times from.
+ * @param xCoordinates the source {@link ResizableIntArray} to read the x-coordinates from.
+ * @param yCoordinates the source {@link ResizableIntArray} to read the y-coordinates from.
+ * @param startPos the starting index of the data in {@code times} and etc.
+ * @param length the number of data to be appended.
+ */
+ public void append(int pointerId, ResizableIntArray times, ResizableIntArray xCoordinates,
+ ResizableIntArray yCoordinates, int startPos, int length) {
+ if (length == 0) {
+ return;
+ }
+ mXCoordinates.append(xCoordinates, startPos, length);
+ mYCoordinates.append(yCoordinates, startPos, length);
+ mPointerIds.fill(pointerId, mPointerIds.getLength(), length);
+ mTimes.append(times, startPos, length);
+ }
+
+ public void reset() {
+ final int defaultCapacity = mDefaultCapacity;
+ mXCoordinates.reset(defaultCapacity);
+ mYCoordinates.reset(defaultCapacity);
+ mPointerIds.reset(defaultCapacity);
+ mTimes.reset(defaultCapacity);
+ }
+
+ public int getPointerSize() {
+ return mXCoordinates.getLength();
+ }
+
+ public int[] getXCoordinates() {
+ return mXCoordinates.getPrimitiveArray();
+ }
+
+ public int[] getYCoordinates() {
+ return mYCoordinates.getPrimitiveArray();
+ }
+
+ public int[] getPointerIds() {
+ return mPointerIds.getPrimitiveArray();
+ }
+
+ public int[] getTimes() {
+ return mTimes.getPrimitiveArray();
+ }
+
+ @Override
+ public String toString() {
+ return "size=" + getPointerSize() + " id=" + mPointerIds + " time=" + mTimes
+ + " x=" + mXCoordinates + " y=" + mYCoordinates;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/InputTypeUtils.java b/java/src/com/android/inputmethod/latin/InputTypeUtils.java
index 40c3b765e..500866a13 100644
--- a/java/src/com/android/inputmethod/latin/InputTypeUtils.java
+++ b/java/src/com/android/inputmethod/latin/InputTypeUtils.java
@@ -18,7 +18,7 @@ package com.android.inputmethod.latin;
import android.text.InputType;
-public class InputTypeUtils implements InputType {
+public final class InputTypeUtils implements InputType {
private static final int WEB_TEXT_PASSWORD_INPUT_TYPE =
TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_WEB_PASSWORD;
private static final int WEB_TEXT_EMAIL_ADDRESS_INPUT_TYPE =
diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java
index 0dcb811b5..d7595bfbe 100644
--- a/java/src/com/android/inputmethod/latin/InputView.java
+++ b/java/src/com/android/inputmethod/latin/InputView.java
@@ -23,8 +23,8 @@ import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
-public class InputView extends LinearLayout {
- private View mSuggestionsContainer;
+public final class InputView extends LinearLayout {
+ private View mSuggestionStripContainer;
private View mKeyboardView;
private int mKeyboardTopPadding;
@@ -43,13 +43,13 @@ public class InputView extends LinearLayout {
@Override
protected void onFinishInflate() {
- mSuggestionsContainer = findViewById(R.id.suggestions_container);
+ mSuggestionStripContainer = findViewById(R.id.suggestions_container);
mKeyboardView = findViewById(R.id.keyboard_view);
}
@Override
public boolean dispatchTouchEvent(MotionEvent me) {
- if (mSuggestionsContainer.getVisibility() == VISIBLE
+ if (mSuggestionStripContainer.getVisibility() == VISIBLE
&& mKeyboardView.getVisibility() == VISIBLE
&& forwardTouchEvent(me)) {
return true;
@@ -57,7 +57,8 @@ public class InputView extends LinearLayout {
return super.dispatchTouchEvent(me);
}
- // The touch events that hit the top padding of keyboard should be forwarded to SuggestionsView.
+ // The touch events that hit the top padding of keyboard should be forwarded to
+ // {@link SuggestionStripView}.
private boolean forwardTouchEvent(MotionEvent me) {
final Rect rect = mInputViewRect;
this.getGlobalVisibleRect(rect);
@@ -96,7 +97,7 @@ public class InputView extends LinearLayout {
}
final Rect receivingRect = mEventReceivingRect;
- mSuggestionsContainer.getGlobalVisibleRect(receivingRect);
+ mSuggestionStripContainer.getGlobalVisibleRect(receivingRect);
final int translatedX = x - receivingRect.left;
final int translatedY;
if (y < forwardingLimitY) {
@@ -106,7 +107,7 @@ public class InputView extends LinearLayout {
translatedY = y - receivingRect.top;
}
me.setLocation(translatedX, translatedY);
- mSuggestionsContainer.dispatchTouchEvent(me);
+ mSuggestionStripContainer.dispatchTouchEvent(me);
return true;
}
}
diff --git a/java/src/com/android/inputmethod/latin/JniUtils.java b/java/src/com/android/inputmethod/latin/JniUtils.java
index 4808b867a..f9305991a 100644
--- a/java/src/com/android/inputmethod/latin/JniUtils.java
+++ b/java/src/com/android/inputmethod/latin/JniUtils.java
@@ -20,7 +20,7 @@ import android.util.Log;
import com.android.inputmethod.latin.define.JniLibName;
-public class JniUtils {
+public final class JniUtils {
private static final String TAG = JniUtils.class.getSimpleName();
private JniUtils() {
@@ -31,11 +31,7 @@ public class JniUtils {
try {
System.loadLibrary(JniLibName.JNI_LIB_NAME);
} catch (UnsatisfiedLinkError ule) {
- Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME);
- if (LatinImeLogger.sDBG) {
- throw new RuntimeException(
- "Could not load native library " + JniLibName.JNI_LIB_NAME);
- }
+ Log.e(TAG, "Could not load native library " + JniLibName.JNI_LIB_NAME, ule);
}
}
}
diff --git a/java/src/com/android/inputmethod/latin/LastComposedWord.java b/java/src/com/android/inputmethod/latin/LastComposedWord.java
index 4e1f5fe92..94cdc9b85 100644
--- a/java/src/com/android/inputmethod/latin/LastComposedWord.java
+++ b/java/src/com/android/inputmethod/latin/LastComposedWord.java
@@ -22,7 +22,7 @@ import android.text.TextUtils;
* This class encapsulates data about a word previously composed, but that has been
* committed already. This is used for resuming suggestion, and cancel auto-correction.
*/
-public class LastComposedWord {
+public final class LastComposedWord {
// COMMIT_TYPE_USER_TYPED_WORD is used when the word committed is the exact typed word, with
// no hinting from the IME. It happens when some external event happens (rotating the device,
// for example) or when auto-correction is off by settings or editor attributes.
@@ -38,32 +38,32 @@ public class LastComposedWord {
// an auto-correction.
public static final int COMMIT_TYPE_CANCEL_AUTO_CORRECT = 3;
- public static final int NOT_A_SEPARATOR = -1;
+ public static final String NOT_A_SEPARATOR = "";
public final int[] mPrimaryKeyCodes;
- public final int[] mXCoordinates;
- public final int[] mYCoordinates;
public final String mTypedWord;
public final String mCommittedWord;
- public final int mSeparatorCode;
+ public final String mSeparatorString;
public final CharSequence mPrevWord;
+ public final InputPointers mInputPointers = new InputPointers(BinaryDictionary.MAX_WORD_LENGTH);
private boolean mActive;
public static final LastComposedWord NOT_A_COMPOSED_WORD =
- new LastComposedWord(null, null, null, "", "", NOT_A_SEPARATOR, null);
+ new LastComposedWord(null, null, "", "", NOT_A_SEPARATOR, null);
// Warning: this is using the passed objects as is and fully expects them to be
// immutable. Do not fiddle with their contents after you passed them to this constructor.
- public LastComposedWord(final int[] primaryKeyCodes, final int[] xCoordinates,
- final int[] yCoordinates, final String typedWord, final String committedWord,
- final int separatorCode, final CharSequence prevWord) {
+ public LastComposedWord(final int[] primaryKeyCodes, final InputPointers inputPointers,
+ final String typedWord, final String committedWord,
+ final String separatorString, final CharSequence prevWord) {
mPrimaryKeyCodes = primaryKeyCodes;
- mXCoordinates = xCoordinates;
- mYCoordinates = yCoordinates;
+ if (inputPointers != null) {
+ mInputPointers.copy(inputPointers);
+ }
mTypedWord = typedWord;
mCommittedWord = committedWord;
- mSeparatorCode = separatorCode;
+ mSeparatorString = separatorString;
mActive = true;
mPrevWord = prevWord;
}
@@ -73,14 +73,14 @@ public class LastComposedWord {
}
public boolean canRevertCommit() {
- return mActive && !TextUtils.isEmpty(mCommittedWord);
+ return mActive && !TextUtils.isEmpty(mCommittedWord) && !didCommitTypedWord();
}
- public boolean didCommitTypedWord() {
+ private boolean didCommitTypedWord() {
return TextUtils.equals(mTypedWord, mCommittedWord);
}
- public static int getSeparatorLength(final int separatorCode) {
- return NOT_A_SEPARATOR == separatorCode ? 0 : 1;
+ public static int getSeparatorLength(final String separatorString) {
+ return StringUtils.codePointCount(separatorString);
}
}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 97e898af9..8ac0cd4ca 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -20,6 +20,7 @@ import static com.android.inputmethod.latin.Constants.ImeOption.FORCE_ASCII;
import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE;
import static com.android.inputmethod.latin.Constants.ImeOption.NO_MICROPHONE_COMPAT;
+import android.app.Activity;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -35,10 +36,11 @@ import android.inputmethodservice.InputMethodService;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.os.Debug;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.SystemClock;
-import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.text.InputType;
import android.text.TextUtils;
@@ -48,31 +50,32 @@ import android.util.Printer;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
-import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
-import android.view.ViewParent;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodSubtype;
import com.android.inputmethod.accessibility.AccessibilityUtils;
import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
import com.android.inputmethod.compat.CompatUtils;
import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.compat.InputMethodServiceCompatUtils;
import com.android.inputmethod.compat.SuggestionSpanUtils;
+import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardActionListener;
import com.android.inputmethod.keyboard.KeyboardId;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.keyboard.KeyboardView;
-import com.android.inputmethod.keyboard.LatinKeyboardView;
+import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
+import com.android.inputmethod.latin.Utils.Stats;
import com.android.inputmethod.latin.define.ProductionFlag;
-import com.android.inputmethod.latin.suggestions.SuggestionsView;
+import com.android.inputmethod.latin.suggestions.SuggestionStripView;
+import com.android.inputmethod.research.ResearchLogger;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -82,8 +85,9 @@ import java.util.Locale;
/**
* Input method implementation for Qwerty'ish keyboard.
*/
-public class LatinIME extends InputMethodService implements KeyboardActionListener,
- SuggestionsView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener {
+public final class LatinIME extends InputMethodService implements KeyboardActionListener,
+ SuggestionStripView.Listener, TargetApplicationGetter.OnTargetApplicationKnownListener,
+ Suggest.SuggestInitializationListener {
private static final String TAG = LatinIME.class.getSimpleName();
private static final boolean TRACE = false;
private static boolean DEBUG;
@@ -103,27 +107,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
*/
private static final String SCHEME_PACKAGE = "package";
- /** Whether to use the binary version of the contacts dictionary */
- public static final boolean USE_BINARY_CONTACTS_DICTIONARY = true;
-
- /** Whether to use the binary version of the user dictionary */
- public static final boolean USE_BINARY_USER_DICTIONARY = true;
-
- // TODO: migrate this to SettingsValues
- private int mSuggestionVisibility;
- private static final int SUGGESTION_VISIBILILTY_SHOW_VALUE
- = R.string.prefs_suggestion_visibility_show_value;
- private static final int SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
- = R.string.prefs_suggestion_visibility_show_only_portrait_value;
- private static final int SUGGESTION_VISIBILILTY_HIDE_VALUE
- = R.string.prefs_suggestion_visibility_hide_value;
-
- private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
- SUGGESTION_VISIBILILTY_SHOW_VALUE,
- SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE,
- SUGGESTION_VISIBILILTY_HIDE_VALUE
- };
-
private static final int SPACE_STATE_NONE = 0;
// Double space: the state where the user pressed space twice quickly, which LatinIME
// resolved as period-space. Undoing this converts the period to a space.
@@ -143,13 +126,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Current space state of the input method. This can be any of the above constants.
private int mSpaceState;
- private SettingsValues mSettingsValues;
- private InputAttributes mInputAttributes;
+ private SettingsValues mCurrentSettings;
private View mExtractArea;
private View mKeyPreviewBackingView;
private View mSuggestionsContainer;
- private SuggestionsView mSuggestionsView;
+ private SuggestionStripView mSuggestionStripView;
/* package for tests */ Suggest mSuggest;
private CompletionInfo[] mApplicationSpecifiedCompletions;
private ApplicationInfo mTargetApplicationInfo;
@@ -162,15 +144,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private boolean mShouldSwitchToLastSubtype = true;
private boolean mIsMainDictionaryAvailable;
- // TODO: revert this back to the concrete class after transition.
- private Dictionary mUserDictionary;
+ private UserBinaryDictionary mUserDictionary;
private UserHistoryDictionary mUserHistoryDictionary;
private boolean mIsUserDictionaryAvailable;
private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
- private WordComposer mWordComposer = new WordComposer();
-
- private int mCorrectionMode;
+ private final WordComposer mWordComposer = new WordComposer();
+ private RichInputConnection mConnection = new RichInputConnection(this);
// Keep track of the last selection range to decide if we need to show word alternatives
private static final int NOT_A_CURSOR_POSITION = -1;
@@ -199,20 +179,24 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private AlertDialog mOptionsDialog;
+ private final boolean mIsHardwareAcceleratedDrawingEnabled;
+
public final UIHandler mHandler = new UIHandler(this);
- public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
- private static final int MSG_UPDATE_SHIFT_STATE = 1;
- private static final int MSG_SPACE_TYPED = 4;
- private static final int MSG_SET_BIGRAM_PREDICTIONS = 5;
- private static final int MSG_PENDING_IMS_CALLBACK = 6;
- private static final int MSG_UPDATE_SUGGESTIONS = 7;
+ public static final class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
+ private static final int MSG_UPDATE_SHIFT_STATE = 0;
+ private static final int MSG_PENDING_IMS_CALLBACK = 1;
+ private static final int MSG_UPDATE_SUGGESTION_STRIP = 2;
+ private static final int MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 3;
+
+ private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
private int mDelayUpdateSuggestions;
private int mDelayUpdateShiftState;
private long mDoubleSpacesTurnIntoPeriodTimeout;
+ private long mDoubleSpaceTimerStart;
- public UIHandler(LatinIME outerInstance) {
+ public UIHandler(final LatinIME outerInstance) {
super(outerInstance);
}
@@ -227,33 +211,33 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
@Override
- public void handleMessage(Message msg) {
+ public void handleMessage(final Message msg) {
final LatinIME latinIme = getOuterInstance();
final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
switch (msg.what) {
- case MSG_UPDATE_SUGGESTIONS:
- latinIme.updateSuggestions();
+ case MSG_UPDATE_SUGGESTION_STRIP:
+ latinIme.updateSuggestionStrip();
break;
case MSG_UPDATE_SHIFT_STATE:
switcher.updateShiftState();
break;
- case MSG_SET_BIGRAM_PREDICTIONS:
- latinIme.updateBigramPredictions();
+ case MSG_SHOW_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
+ latinIme.showGesturePreviewAndSuggestionStrip((SuggestedWords)msg.obj,
+ msg.arg1 == ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT);
break;
}
}
- public void postUpdateSuggestions() {
- removeMessages(MSG_UPDATE_SUGGESTIONS);
- sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), mDelayUpdateSuggestions);
+ public void postUpdateSuggestionStrip() {
+ sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
}
- public void cancelUpdateSuggestions() {
- removeMessages(MSG_UPDATE_SUGGESTIONS);
+ public void cancelUpdateSuggestionStrip() {
+ removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
}
public boolean hasPendingUpdateSuggestions() {
- return hasMessages(MSG_UPDATE_SUGGESTIONS);
+ return hasMessages(MSG_UPDATE_SUGGESTION_STRIP);
}
public void postUpdateShiftState() {
@@ -265,26 +249,26 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
removeMessages(MSG_UPDATE_SHIFT_STATE);
}
- public void postUpdateBigramPredictions() {
- removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
- sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS), mDelayUpdateSuggestions);
- }
-
- public void cancelUpdateBigramPredictions() {
- removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
+ public void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
+ 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();
}
public void startDoubleSpacesTimer() {
- removeMessages(MSG_SPACE_TYPED);
- sendMessageDelayed(obtainMessage(MSG_SPACE_TYPED), mDoubleSpacesTurnIntoPeriodTimeout);
+ mDoubleSpaceTimerStart = SystemClock.uptimeMillis();
}
public void cancelDoubleSpacesTimer() {
- removeMessages(MSG_SPACE_TYPED);
+ mDoubleSpaceTimerStart = 0;
}
public boolean isAcceptingDoubleSpaces() {
- return hasMessages(MSG_SPACE_TYPED);
+ return SystemClock.uptimeMillis() - mDoubleSpaceTimerStart
+ < mDoubleSpacesTurnIntoPeriodTimeout;
}
// Working variables for the following methods.
@@ -311,7 +295,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mHasPendingStartInput = false;
}
- private void executePendingImsCallback(LatinIME latinIme, EditorInfo editorInfo,
+ private void executePendingImsCallback(final LatinIME latinIme, final EditorInfo editorInfo,
boolean restarting) {
if (mHasPendingFinishInputView)
latinIme.onFinishInputViewInternal(mHasPendingFinishInput);
@@ -322,7 +306,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
resetPendingImsCallback();
}
- public void onStartInput(EditorInfo editorInfo, boolean restarting) {
+ public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
// Typically this is the second onStartInput after orientation changed.
mHasPendingStartInput = true;
@@ -338,7 +322,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
+ public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
if (hasMessages(MSG_PENDING_IMS_CALLBACK)
&& KeyboardId.equivalentEditorInfoForKeyboard(editorInfo, mAppliedEditorInfo)) {
// Typically this is the second onStartInputView after orientation changed.
@@ -358,7 +342,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- public void onFinishInputView(boolean finishingInput) {
+ public void onFinishInputView(final boolean finishingInput) {
if (hasMessages(MSG_PENDING_IMS_CALLBACK)) {
// Typically this is the first onFinishInputView after orientation changed.
mHasPendingFinishInputView = true;
@@ -385,6 +369,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
super();
mSubtypeSwitcher = SubtypeSwitcher.getInstance();
mKeyboardSwitcher = KeyboardSwitcher.getInstance();
+ mIsHardwareAcceleratedDrawingEnabled =
+ InputMethodServiceCompatUtils.enableHardwareAcceleration(this);
+ Log.i(TAG, "Hardware accelerated drawing: " + mIsHardwareAcceleratedDrawingEnabled);
}
@Override
@@ -393,7 +380,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mPrefs = prefs;
LatinImeLogger.init(this, prefs);
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.init(this, prefs);
+ ResearchLogger.getInstance().init(this, prefs);
}
InputMethodManagerCompatWrapper.init(this);
SubtypeSwitcher.init(this);
@@ -411,22 +398,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
loadSettings();
- ImfUtils.setAdditionalInputMethodSubtypes(this, mSettingsValues.getAdditionalSubtypes());
-
- // TODO: remove the following when it's not needed by updateCorrectionMode() any more
- mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
- updateCorrectionMode();
+ ImfUtils.setAdditionalInputMethodSubtypes(this, mCurrentSettings.getAdditionalSubtypes());
- Utils.GCUtils.getInstance().reset();
- boolean tryGC = true;
- for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
- try {
- initSuggest();
- tryGC = false;
- } catch (OutOfMemoryError e) {
- tryGC = Utils.GCUtils.getInstance().tryGCOrWait("InitSuggest", e);
- }
- }
+ initSuggest();
mDisplayOrientation = res.getConfiguration().orientation;
@@ -450,46 +424,58 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
// Has to be package-visible for unit tests
- /* package */ void loadSettings() {
+ /* package for test */
+ void loadSettings() {
// Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
// is not guaranteed. It may even be called at the same time on a different thread.
if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ final InputAttributes inputAttributes =
+ new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
final RunInLocale<SettingsValues> job = new RunInLocale<SettingsValues>() {
@Override
protected SettingsValues job(Resources res) {
- return new SettingsValues(mPrefs, LatinIME.this);
+ return new SettingsValues(mPrefs, inputAttributes, LatinIME.this);
}
};
- mSettingsValues = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
- mFeedbackManager = new AudioAndHapticFeedbackManager(this, mSettingsValues);
+ mCurrentSettings = job.runInLocale(mResources, mSubtypeSwitcher.getCurrentSubtypeLocale());
+ mFeedbackManager = new AudioAndHapticFeedbackManager(this, mCurrentSettings);
resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
}
+ // Note that this method is called from a non-UI thread.
+ @Override
+ public void onUpdateMainDictionaryAvailability(final boolean isMainDictionaryAvailable) {
+ mIsMainDictionaryAvailable = isMainDictionaryAvailable;
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView != null) {
+ mainKeyboardView.setMainDictionaryAvailability(isMainDictionaryAvailable);
+ }
+ }
+
private void initSuggest() {
final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
final String localeStr = subtypeLocale.toString();
- final Dictionary oldContactsDictionary;
+ final ContactsBinaryDictionary oldContactsDictionary;
if (mSuggest != null) {
oldContactsDictionary = mSuggest.getContactsDictionary();
mSuggest.close();
} else {
oldContactsDictionary = null;
}
- mSuggest = new Suggest(this, subtypeLocale);
- if (mSettingsValues.mAutoCorrectEnabled) {
- mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
+ mSuggest = new Suggest(this /* Context */, subtypeLocale,
+ this /* SuggestInitializationListener */);
+ if (mCurrentSettings.mCorrectionEnabled) {
+ mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
}
mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
-
- if (USE_BINARY_USER_DICTIONARY) {
- mUserDictionary = new UserBinaryDictionary(this, localeStr);
- mIsUserDictionaryAvailable = ((UserBinaryDictionary)mUserDictionary).isEnabled();
- } else {
- mUserDictionary = new UserDictionary(this, localeStr);
- mIsUserDictionaryAvailable = ((UserDictionary)mUserDictionary).isEnabled();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().initSuggest(mSuggest);
}
+
+ mUserDictionary = new UserBinaryDictionary(this, localeStr);
+ mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
mSuggest.setUserDictionary(mUserDictionary);
resetContactsDictionary(oldContactsDictionary);
@@ -497,44 +483,43 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
// is not guaranteed. It may even be called at the same time on a different thread.
if (null == mPrefs) mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
- mUserHistoryDictionary = UserHistoryDictionary.getInstance(
- this, localeStr, Suggest.DIC_USER_HISTORY, mPrefs);
+ mUserHistoryDictionary = UserHistoryDictionary.getInstance(this, localeStr, mPrefs);
mSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
}
/**
* Resets the contacts dictionary in mSuggest according to the user settings.
*
- * This method takes an optional contacts dictionary to use. Since the contacts dictionary
- * does not depend on the locale, it can be reused across different instances of Suggest.
- * The dictionary will also be opened or closed as necessary depending on the settings.
+ * This method takes an optional contacts dictionary to use when the locale hasn't changed
+ * since the contacts dictionary can be opened or closed as necessary depending on the settings.
*
* @param oldContactsDictionary an optional dictionary to use, or null
*/
- private void resetContactsDictionary(final Dictionary oldContactsDictionary) {
- final boolean shouldSetDictionary = (null != mSuggest && mSettingsValues.mUseContactsDict);
+ private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary) {
+ final boolean shouldSetDictionary = (null != mSuggest && mCurrentSettings.mUseContactsDict);
- final Dictionary dictionaryToUse;
+ final ContactsBinaryDictionary dictionaryToUse;
if (!shouldSetDictionary) {
// Make sure the dictionary is closed. If it is already closed, this is a no-op,
// so it's safe to call it anyways.
if (null != oldContactsDictionary) oldContactsDictionary.close();
dictionaryToUse = null;
- } else if (null != oldContactsDictionary) {
- // Make sure the old contacts dictionary is opened. If it is already open, this is a
- // no-op, so it's safe to call it anyways.
- if (USE_BINARY_CONTACTS_DICTIONARY) {
- ((ContactsBinaryDictionary)oldContactsDictionary).reopen(this);
- } else {
- ((ContactsDictionary)oldContactsDictionary).reopen(this);
- }
- dictionaryToUse = oldContactsDictionary;
} else {
- if (USE_BINARY_CONTACTS_DICTIONARY) {
- dictionaryToUse = new ContactsBinaryDictionary(this, Suggest.DIC_CONTACTS,
- mSubtypeSwitcher.getCurrentSubtypeLocale());
+ final Locale locale = mSubtypeSwitcher.getCurrentSubtypeLocale();
+ if (null != oldContactsDictionary) {
+ if (!oldContactsDictionary.mLocale.equals(locale)) {
+ // If the locale has changed then recreate the contacts dictionary. This
+ // allows locale dependent rules for handling bigram name predictions.
+ oldContactsDictionary.close();
+ dictionaryToUse = new ContactsBinaryDictionary(this, locale);
+ } else {
+ // Make sure the old contacts dictionary is opened. If it is already open,
+ // this is a no-op, so it's safe to call it anyways.
+ oldContactsDictionary.reopen(this);
+ dictionaryToUse = oldContactsDictionary;
+ }
} else {
- dictionaryToUse = new ContactsDictionary(this, Suggest.DIC_CONTACTS);
+ dictionaryToUse = new ContactsBinaryDictionary(this, locale);
}
}
@@ -545,7 +530,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
/* package private */ void resetSuggestMainDict() {
final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
- mSuggest.resetMainDict(this, subtypeLocale);
+ mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */);
mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
}
@@ -563,59 +548,64 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
@Override
- public void onConfigurationChanged(Configuration conf) {
- mSubtypeSwitcher.onConfigurationChanged(conf);
+ public void onConfigurationChanged(final Configuration conf) {
+ // System locale has been changed. Needs to reload keyboard.
+ if (mSubtypeSwitcher.onConfigurationChanged(conf, this)) {
+ loadKeyboard();
+ }
// If orientation changed while predicting, commit the change
if (mDisplayOrientation != conf.orientation) {
mDisplayOrientation = conf.orientation;
mHandler.startOrientationChanging();
- final InputConnection ic = getCurrentInputConnection();
- commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
- if (ic != null) ic.finishComposingText(); // For voice input
- if (isShowingOptionDialog())
+ mConnection.beginBatchEdit();
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ mConnection.finishComposingText();
+ mConnection.endBatchEdit();
+ if (isShowingOptionDialog()) {
mOptionsDialog.dismiss();
+ }
}
super.onConfigurationChanged(conf);
}
@Override
public View onCreateInputView() {
- return mKeyboardSwitcher.onCreateInputView();
+ return mKeyboardSwitcher.onCreateInputView(mIsHardwareAcceleratedDrawingEnabled);
}
@Override
- public void setInputView(View view) {
+ public void setInputView(final View view) {
super.setInputView(view);
mExtractArea = getWindow().getWindow().getDecorView()
.findViewById(android.R.id.extractArea);
mKeyPreviewBackingView = view.findViewById(R.id.key_preview_backing);
mSuggestionsContainer = view.findViewById(R.id.suggestions_container);
- mSuggestionsView = (SuggestionsView) view.findViewById(R.id.suggestions_view);
- if (mSuggestionsView != null)
- mSuggestionsView.setListener(this, view);
+ mSuggestionStripView = (SuggestionStripView)view.findViewById(R.id.suggestion_strip_view);
+ if (mSuggestionStripView != null)
+ mSuggestionStripView.setListener(this, view);
if (LatinImeLogger.sVISUALDEBUG) {
mKeyPreviewBackingView.setBackgroundColor(0x10FF0000);
}
}
@Override
- public void setCandidatesView(View view) {
+ public void setCandidatesView(final View view) {
// To ensure that CandidatesView will never be set.
return;
}
@Override
- public void onStartInput(EditorInfo editorInfo, boolean restarting) {
+ public void onStartInput(final EditorInfo editorInfo, final boolean restarting) {
mHandler.onStartInput(editorInfo, restarting);
}
@Override
- public void onStartInputView(EditorInfo editorInfo, boolean restarting) {
+ public void onStartInputView(final EditorInfo editorInfo, final boolean restarting) {
mHandler.onStartInputView(editorInfo, restarting);
}
@Override
- public void onFinishInputView(boolean finishingInput) {
+ public void onFinishInputView(final boolean finishingInput) {
mHandler.onFinishInputView(finishingInput);
}
@@ -625,21 +615,22 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
@Override
- public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype) {
+ public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) {
// Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
// is not guaranteed. It may even be called at the same time on a different thread.
mSubtypeSwitcher.updateSubtype(subtype);
+ loadKeyboard();
}
- private void onStartInputInternal(EditorInfo editorInfo, boolean restarting) {
+ private void onStartInputInternal(final EditorInfo editorInfo, final boolean restarting) {
super.onStartInput(editorInfo, restarting);
}
@SuppressWarnings("deprecation")
- private void onStartInputViewInternal(EditorInfo editorInfo, boolean restarting) {
+ private void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restarting) {
super.onStartInputView(editorInfo, restarting);
final KeyboardSwitcher switcher = mKeyboardSwitcher;
- LatinKeyboardView inputView = switcher.getKeyboardView();
+ final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
if (editorInfo == null) {
Log.e(TAG, "Null EditorInfo in onStartInputView()");
@@ -682,93 +673,143 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
LatinImeLogger.onStartInputView(editorInfo);
// In landscape mode, this method gets called without the input view being created.
- if (inputView == null) {
+ if (mainKeyboardView == null) {
return;
}
// Forward this event to the accessibility utilities, if enabled.
final AccessibilityUtils accessUtils = AccessibilityUtils.getInstance();
if (accessUtils.isTouchExplorationEnabled()) {
- accessUtils.onStartInputViewInternal(editorInfo, restarting);
+ accessUtils.onStartInputViewInternal(mainKeyboardView, editorInfo, restarting);
}
- mSubtypeSwitcher.updateParametersOnStartInputView();
+ final boolean selectionChanged = mLastSelectionStart != editorInfo.initialSelStart
+ || mLastSelectionEnd != editorInfo.initialSelEnd;
+ final boolean inputTypeChanged = !mCurrentSettings.isSameInputType(editorInfo);
+ final boolean isDifferentTextField = !restarting || inputTypeChanged;
+ if (isDifferentTextField) {
+ final boolean currentSubtypeEnabled = mSubtypeSwitcher
+ .updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled();
+ if (!currentSubtypeEnabled) {
+ // Current subtype is disabled. Needs to update subtype and keyboard.
+ final InputMethodSubtype newSubtype = ImfUtils.getCurrentInputMethodSubtype(
+ this, mSubtypeSwitcher.getNoLanguageSubtype());
+ mSubtypeSwitcher.updateSubtype(newSubtype);
+ loadKeyboard();
+ }
+ }
// The EditorInfo might have a flag that affects fullscreen mode.
// Note: This call should be done by InputMethodService?
updateFullscreenMode();
- mLastSelectionStart = editorInfo.initialSelStart;
- mLastSelectionEnd = editorInfo.initialSelEnd;
- mInputAttributes = new InputAttributes(editorInfo, isFullscreenMode());
mApplicationSpecifiedCompletions = null;
- inputView.closing();
- mEnteredText = null;
- resetComposingState(true /* alsoResetLastComposedWord */);
- mDeleteCount = 0;
- mSpaceState = SPACE_STATE_NONE;
-
- loadSettings();
- updateCorrectionMode();
- updateSuggestionVisibility(mResources);
+ if (isDifferentTextField || selectionChanged) {
+ // If the selection changed, we reset the input state. Essentially, we come here with
+ // restarting == true when the app called setText() or similar. We should reset the
+ // state if the app set the text to something else, but keep it if it set a suggestion
+ // or something.
+ mEnteredText = null;
+ resetComposingState(true /* alsoResetLastComposedWord */);
+ mDeleteCount = 0;
+ mSpaceState = SPACE_STATE_NONE;
- if (mSuggest != null && mSettingsValues.mAutoCorrectEnabled) {
- mSuggest.setAutoCorrectionThreshold(mSettingsValues.mAutoCorrectionThreshold);
+ if (mSuggestionStripView != null) {
+ // This will set the punctuation suggestions if next word suggestion is off;
+ // otherwise it will clear the suggestion strip.
+ setPunctuationSuggestions();
+ }
}
- switcher.loadKeyboard(editorInfo, mSettingsValues);
+ mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart);
- if (mSuggestionsView != null)
- mSuggestionsView.clear();
+ if (isDifferentTextField) {
+ mainKeyboardView.closing();
+ loadSettings();
+
+ if (mSuggest != null && mCurrentSettings.mCorrectionEnabled) {
+ mSuggest.setAutoCorrectionThreshold(mCurrentSettings.mAutoCorrectionThreshold);
+ }
+
+ switcher.loadKeyboard(editorInfo, mCurrentSettings);
+ }
setSuggestionStripShownInternal(
isSuggestionsStripVisible(), /* needsInputViewShown */ false);
- // Delay updating suggestions because keyboard input view may not be shown at this point.
- mHandler.postUpdateSuggestions();
+
+ mLastSelectionStart = editorInfo.initialSelStart;
+ mLastSelectionEnd = editorInfo.initialSelEnd;
+ // If we come here something in the text state is very likely to have changed.
+ // We should update the shift state regardless of whether we are restarting or not, because
+ // this is not perceived as a layout change that may be disruptive like we may have with
+ // switcher.loadKeyboard; in apps like Talk, we come here when the text is sent and the
+ // field gets emptied and we need to re-evaluate the shift state, but not the whole layout
+ // which would be disruptive.
+ // Space state must be updated before calling updateShiftState
+ mKeyboardSwitcher.updateShiftState();
+
+ mHandler.cancelUpdateSuggestionStrip();
mHandler.cancelDoubleSpacesTimer();
- inputView.setKeyPreviewPopupEnabled(mSettingsValues.mKeyPreviewPopupOn,
- mSettingsValues.mKeyPreviewPopupDismissDelay);
- inputView.setProximityCorrectionEnabled(true);
+ mainKeyboardView.setMainDictionaryAvailability(mIsMainDictionaryAvailable);
+ mainKeyboardView.setKeyPreviewPopupEnabled(mCurrentSettings.mKeyPreviewPopupOn,
+ mCurrentSettings.mKeyPreviewPopupDismissDelay);
+ mainKeyboardView.setGestureHandlingEnabledByUser(mCurrentSettings.mGestureInputEnabled);
+ mainKeyboardView.setGesturePreviewMode(mCurrentSettings.mGesturePreviewTrailEnabled,
+ mCurrentSettings.mGestureFloatingPreviewTextEnabled);
if (TRACE) Debug.startMethodTracing("/data/trace/latinime");
}
+ // Callback for the TargetApplicationGetter
+ @Override
public void onTargetApplicationKnown(final ApplicationInfo info) {
mTargetApplicationInfo = info;
}
@Override
public void onWindowHidden() {
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onWindowHidden(mLastSelectionStart, mLastSelectionEnd,
+ getCurrentInputConnection());
+ }
super.onWindowHidden();
- KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView != null) inputView.closing();
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView != null) {
+ mainKeyboardView.closing();
+ }
}
private void onFinishInputInternal() {
super.onFinishInput();
LatinImeLogger.commit();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().latinIME_onFinishInputInternal();
+ }
- KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView != null) inputView.closing();
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView != null) {
+ mainKeyboardView.closing();
+ }
}
- private void onFinishInputViewInternal(boolean finishingInput) {
+ private void onFinishInputViewInternal(final boolean finishingInput) {
super.onFinishInputView(finishingInput);
mKeyboardSwitcher.onFinishInputView();
- KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView != null) inputView.cancelAllMessages();
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView != null) {
+ mainKeyboardView.cancelAllMessages();
+ }
// Remove pending messages related to update suggestions
- mHandler.cancelUpdateSuggestions();
+ mHandler.cancelUpdateSuggestionStrip();
}
@Override
- public void onUpdateSelection(int oldSelStart, int oldSelEnd,
- int newSelStart, int newSelEnd,
- int composingSpanStart, int composingSpanEnd) {
+ public void onUpdateSelection(final int oldSelStart, final int oldSelEnd,
+ final int newSelStart, final int newSelEnd,
+ final int composingSpanStart, final int composingSpanEnd) {
super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
composingSpanStart, composingSpanEnd);
-
if (DEBUG) {
Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
+ ", ose=" + oldSelEnd
@@ -780,9 +821,16 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
+ ", ce=" + composingSpanEnd);
}
if (ProductionFlag.IS_EXPERIMENTAL) {
+ final boolean expectingUpdateSelectionFromLogger =
+ ResearchLogger.getAndClearLatinIMEExpectingUpdateSelection();
ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
- composingSpanEnd);
+ composingSpanEnd, mExpectingUpdateSelection,
+ expectingUpdateSelectionFromLogger, mConnection);
+ if (expectingUpdateSelectionFromLogger) {
+ // TODO: Investigate. Quitting now sounds wrong - we won't do the resetting work
+ return;
+ }
}
// TODO: refactor the following code to be less contrived.
@@ -799,7 +847,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// we know for sure the cursor moved while we were composing and we should reset
// the state.
final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
- if (!mExpectingUpdateSelection) {
+ if (!mExpectingUpdateSelection
+ && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart)) {
// TAKE CARE: there is a race condition when we enter this test even when the user
// did not explicitly move the cursor. This happens when typing fast, where two keys
// turn this flag on in succession and both onUpdateSelection() calls arrive after
@@ -809,16 +858,25 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// TODO: the following is probably better done in resetEntireInputState().
// it should only happen when the cursor moved, and the very purpose of the
// test below is to narrow down whether this happened or not. Likewise with
- // the call to postUpdateShiftState.
+ // the call to updateShiftState.
// We set this to NONE because after a cursor move, we don't want the space
// state-related special processing to kick in.
mSpaceState = SPACE_STATE_NONE;
if ((!mWordComposer.isComposingWord()) || selectionChanged || noComposingSpan) {
- resetEntireInputState();
+ // If we are composing a word and moving the cursor, we would want to set a
+ // suggestion span for recorrection to work correctly. Unfortunately, that
+ // would involve the keyboard committing some new text, which would move the
+ // cursor back to where it was. Latin IME could then fix the position of the cursor
+ // again, but the asynchronous nature of the calls results in this wreaking havoc
+ // with selection on double tap and the like.
+ // Another option would be to send suggestions each time we set the composing
+ // text, but that is probably too expensive to do, so we decided to leave things
+ // as is.
+ resetEntireInputState(newSelStart);
}
- mHandler.postUpdateShiftState();
+ mKeyboardSwitcher.updateShiftState();
}
mExpectingUpdateSelection = false;
// TODO: Decide to call restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() or not
@@ -841,7 +899,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
*/
@Override
public void onExtractedTextClicked() {
- if (isSuggestionsRequested()) return;
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
super.onExtractedTextClicked();
}
@@ -856,8 +914,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
* cause the suggestions strip to disappear and re-appear.
*/
@Override
- public void onExtractedCursorMovement(int dx, int dy) {
- if (isSuggestionsRequested()) return;
+ public void onExtractedCursorMovement(final int dx, final int dy) {
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) return;
super.onExtractedCursorMovement(dx, dy);
}
@@ -876,7 +934,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
@Override
- public void onDisplayCompletions(CompletionInfo[] applicationSpecifiedCompletions) {
+ public void onDisplayCompletions(final CompletionInfo[] applicationSpecifiedCompletions) {
if (DEBUG) {
Log.i(TAG, "Received completions:");
if (applicationSpecifiedCompletions != null) {
@@ -885,43 +943,46 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
}
+ if (!mCurrentSettings.isApplicationSpecifiedCompletionsOn()) return;
+ mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
+ if (applicationSpecifiedCompletions == null) {
+ clearSuggestionStrip();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onDisplayCompletions(null);
+ }
+ return;
+ }
+
+ final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
+ SuggestedWords.getFromApplicationSpecifiedCompletions(
+ applicationSpecifiedCompletions);
+ final SuggestedWords suggestedWords = new SuggestedWords(
+ applicationSuggestedWords,
+ false /* typedWordValid */,
+ false /* hasAutoCorrectionCandidate */,
+ false /* isPunctuationSuggestions */,
+ false /* isObsoleteSuggestions */,
+ false /* isPrediction */);
+ // When in fullscreen mode, show completions generated by the application
+ final boolean isAutoCorrection = false;
+ setSuggestionStrip(suggestedWords, isAutoCorrection);
+ setAutoCorrectionIndicator(isAutoCorrection);
+ // TODO: is this the right thing to do? What should we auto-correct to in
+ // this case? This says to keep whatever the user typed.
+ mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
+ setSuggestionStripShown(true);
if (ProductionFlag.IS_EXPERIMENTAL) {
ResearchLogger.latinIME_onDisplayCompletions(applicationSpecifiedCompletions);
}
- if (mInputAttributes.mApplicationSpecifiedCompletionOn) {
- mApplicationSpecifiedCompletions = applicationSpecifiedCompletions;
- if (applicationSpecifiedCompletions == null) {
- clearSuggestions();
- return;
- }
+ }
- final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
- SuggestedWords.getFromApplicationSpecifiedCompletions(
- applicationSpecifiedCompletions);
- final SuggestedWords suggestedWords = new SuggestedWords(
- applicationSuggestedWords,
- false /* typedWordValid */,
- false /* hasAutoCorrectionCandidate */,
- false /* allowsToBeAutoCorrected */,
- false /* isPunctuationSuggestions */,
- false /* isObsoleteSuggestions */,
- false /* isPrediction */);
- // When in fullscreen mode, show completions generated by the application
- final boolean isAutoCorrection = false;
- setSuggestions(suggestedWords, isAutoCorrection);
- setAutoCorrectionIndicator(isAutoCorrection);
- // TODO: is this the right thing to do? What should we auto-correct to in
- // this case? This says to keep whatever the user typed.
- mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
- setSuggestionStripShown(true);
- }
- }
-
- private void setSuggestionStripShownInternal(boolean shown, boolean needsInputViewShown) {
+ private void setSuggestionStripShownInternal(final boolean shown,
+ final boolean needsInputViewShown) {
// TODO: Modify this if we support suggestions with hard keyboard
if (onEvaluateInputViewShown() && mSuggestionsContainer != null) {
- final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
- final boolean inputViewShown = (keyboardView != null) ? keyboardView.isShown() : false;
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ final boolean inputViewShown = (mainKeyboardView != null)
+ ? mainKeyboardView.isShown() : false;
final boolean shouldShowSuggestions = shown
&& (needsInputViewShown ? inputViewShown : true);
if (isFullscreenMode()) {
@@ -934,7 +995,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
- private void setSuggestionStripShown(boolean shown) {
+ private void setSuggestionStripShown(final boolean shown) {
setSuggestionStripShownInternal(shown, /* needsInputViewShown */true);
}
@@ -944,11 +1005,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return currentHeight;
}
- final KeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
- if (keyboardView == null) {
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView == null) {
return 0;
}
- final int keyboardHeight = keyboardView.getHeight();
+ final int keyboardHeight = mainKeyboardView.getHeight();
final int suggestionsHeight = mSuggestionsContainer.getHeight();
final int displayHeight = mResources.getDisplayMetrics().heightPixels;
final Rect rect = new Rect();
@@ -958,17 +1019,18 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
- keyboardHeight;
final LayoutParams params = mKeyPreviewBackingView.getLayoutParams();
- params.height = mSuggestionsView.setMoreSuggestionsHeight(remainingHeight);
+ params.height = mSuggestionStripView.setMoreSuggestionsHeight(remainingHeight);
mKeyPreviewBackingView.setLayoutParams(params);
return params.height;
}
@Override
- public void onComputeInsets(InputMethodService.Insets outInsets) {
+ public void onComputeInsets(final InputMethodService.Insets outInsets) {
super.onComputeInsets(outInsets);
- final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView == null || mSuggestionsContainer == null)
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView == null || mSuggestionsContainer == null) {
return;
+ }
final int adjustedBackingHeight = getAdjustedBackingViewHeight();
final boolean backingGone = (mKeyPreviewBackingView.getVisibility() == View.GONE);
final int backingHeight = backingGone ? 0 : adjustedBackingHeight;
@@ -981,13 +1043,12 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final int extraHeight = extractHeight + backingHeight + suggestionsHeight;
int touchY = extraHeight;
// Need to set touchable region only if input view is being shown
- final LatinKeyboardView keyboardView = mKeyboardSwitcher.getKeyboardView();
- if (keyboardView != null && keyboardView.isShown()) {
+ if (mainKeyboardView.isShown()) {
if (mSuggestionsContainer.getVisibility() == View.VISIBLE) {
touchY -= suggestionsHeight;
}
- final int touchWidth = inputView.getWidth();
- final int touchHeight = inputView.getHeight() + extraHeight
+ final int touchWidth = mainKeyboardView.getWidth();
+ final int touchHeight = mainKeyboardView.getHeight() + extraHeight
// Extend touchable region below the keyboard.
+ EXTENDED_TOUCHABLE_REGION_HEIGHT;
outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
@@ -1001,8 +1062,17 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
public boolean onEvaluateFullscreenMode() {
// Reread resource value here, because this method is called by framework anytime as needed.
final boolean isFullscreenModeAllowed =
- mSettingsValues.isFullscreenModeAllowed(getResources());
- return super.onEvaluateFullscreenMode() && isFullscreenModeAllowed;
+ mCurrentSettings.isFullscreenModeAllowed(getResources());
+ if (super.onEvaluateFullscreenMode() && isFullscreenModeAllowed) {
+ // TODO: Remove this hack. Actually we should not really assume NO_EXTRACT_UI
+ // implies NO_FULLSCREEN. However, the framework mistakenly does. i.e. NO_EXTRACT_UI
+ // without NO_FULLSCREEN doesn't work as expected. Because of this we need this
+ // hack for now. Let's get rid of this once the framework gets fixed.
+ final EditorInfo ei = getCurrentInputEditorInfo();
+ return !(ei != null && ((ei.imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0));
+ } else {
+ return false;
+ }
}
@Override
@@ -1016,15 +1086,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
// This will reset the whole input state to the starting state. It will clear
- // the composing word, reset the last composed word, tell the inputconnection
- // and the composingStateManager about it.
- private void resetEntireInputState() {
+ // the composing word, reset the last composed word, tell the inputconnection about it.
+ private void resetEntireInputState(final int newCursorPosition) {
resetComposingState(true /* alsoResetLastComposedWord */);
- updateSuggestions();
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- ic.finishComposingText();
+ if (mCurrentSettings.mBigramPredictionEnabled) {
+ clearSuggestionStrip();
+ } else {
+ setSuggestionStrip(mCurrentSettings.mSuggestPuncList, false);
}
+ mConnection.resetCachesUponCursorMove(newCursorPosition);
}
private void resetComposingState(final boolean alsoResetLastComposedWord) {
@@ -1033,86 +1103,66 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
}
- public void commitTyped(final InputConnection ic, final int separatorCode) {
+ private void commitTyped(final String separatorString) {
if (!mWordComposer.isComposingWord()) return;
final CharSequence typedWord = mWordComposer.getTypedWord();
if (typedWord.length() > 0) {
- if (ic != null) {
- ic.commitText(typedWord, 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitText(typedWord);
- }
- }
- final CharSequence prevWord = addToUserHistoryDictionary(typedWord);
- mLastComposedWord = mWordComposer.commitWord(
- LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(),
- separatorCode, prevWord);
+ commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD,
+ separatorString);
}
- updateSuggestions();
}
+ // Called from the KeyboardSwitcher which needs to know auto caps state to display
+ // the right layout.
public int getCurrentAutoCapsState() {
- if (!mSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
+ if (!mCurrentSettings.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
final EditorInfo ei = getCurrentInputEditorInfo();
if (ei == null) return Constants.TextUtils.CAP_MODE_OFF;
-
final int inputType = ei.inputType;
- if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
- return TextUtils.CAP_MODE_CHARACTERS;
- }
-
- final boolean noNeedToCheckCapsMode = (inputType & (InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
- | InputType.TYPE_TEXT_FLAG_CAP_WORDS)) == 0;
- if (noNeedToCheckCapsMode) return Constants.TextUtils.CAP_MODE_OFF;
-
- // Avoid making heavy round-trip IPC calls of {@link InputConnection#getCursorCapsMode}
- // unless needed.
- if (mWordComposer.isComposingWord()) return Constants.TextUtils.CAP_MODE_OFF;
+ // Warning: this depends on mSpaceState, which may not be the most current value. If
+ // mSpaceState gets updated later, whoever called this may need to be told about it.
+ return mConnection.getCursorCapsMode(inputType, mSubtypeSwitcher.getCurrentSubtypeLocale(),
+ SPACE_STATE_PHANTOM == mSpaceState);
+ }
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return Constants.TextUtils.CAP_MODE_OFF;
- // TODO: This blocking IPC call is heavy. Consider doing this without using IPC calls.
- // Note: getCursorCapsMode() returns the current capitalization mode that is any
- // combination of CAP_MODE_CHARACTERS, CAP_MODE_WORDS, and CAP_MODE_SENTENCES. 0 means none
- // of them.
- return ic.getCursorCapsMode(inputType);
+ // Factor in auto-caps and manual caps and compute the current caps mode.
+ private int getActualCapsMode() {
+ final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode();
+ if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
+ final int auto = getCurrentAutoCapsState();
+ if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
+ return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
+ }
+ if (0 != auto) return WordComposer.CAPS_MODE_AUTO_SHIFTED;
+ return WordComposer.CAPS_MODE_OFF;
}
- // "ic" may be null
- private void swapSwapperAndSpaceWhileInBatchEdit(final InputConnection ic) {
- if (null == ic) return;
- CharSequence lastTwo = ic.getTextBeforeCursor(2, 0);
+ private void swapSwapperAndSpace() {
+ CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
// It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
if (lastTwo != null && lastTwo.length() == 2
&& lastTwo.charAt(0) == Keyboard.CODE_SPACE) {
- ic.deleteSurroundingText(2, 0);
+ mConnection.deleteSurroundingText(2, 0);
+ mConnection.commitText(lastTwo.charAt(1) + " ", 1);
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(2);
- }
- ic.commitText(lastTwo.charAt(1) + " ", 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_swapSwapperAndSpaceWhileInBatchEdit();
+ ResearchLogger.latinIME_swapSwapperAndSpace();
}
mKeyboardSwitcher.updateShiftState();
}
}
- private boolean maybeDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
- if (mCorrectionMode == Suggest.CORRECTION_NONE) return false;
- if (ic == null) return false;
- final CharSequence lastThree = ic.getTextBeforeCursor(3, 0);
+ private boolean maybeDoubleSpace() {
+ if (!mCurrentSettings.mCorrectionEnabled) return false;
+ if (!mHandler.isAcceptingDoubleSpaces()) return false;
+ final CharSequence lastThree = mConnection.getTextBeforeCursor(3, 0);
if (lastThree != null && lastThree.length() == 3
&& canBeFollowedByPeriod(lastThree.charAt(0))
&& lastThree.charAt(1) == Keyboard.CODE_SPACE
- && lastThree.charAt(2) == Keyboard.CODE_SPACE
- && mHandler.isAcceptingDoubleSpaces()) {
+ && lastThree.charAt(2) == Keyboard.CODE_SPACE) {
mHandler.cancelDoubleSpacesTimer();
- ic.deleteSurroundingText(2, 0);
- ic.commitText(". ", 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_doubleSpaceAutoPeriod();
- }
+ mConnection.deleteSurroundingText(2, 0);
+ mConnection.commitText(". ", 1);
mKeyboardSwitcher.updateShiftState();
return true;
}
@@ -1131,33 +1181,15 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
|| codePoint == Keyboard.CODE_CLOSING_ANGLE_BRACKET;
}
- // "ic" may be null
- private static void removeTrailingSpaceWhileInBatchEdit(final InputConnection ic) {
- if (ic == null) return;
- final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
- if (lastOne != null && lastOne.length() == 1
- && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
- ic.deleteSurroundingText(1, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
- }
- }
- }
-
+ // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
+ // pressed.
@Override
- public boolean addWordToDictionary(String word) {
- if (USE_BINARY_USER_DICTIONARY) {
- ((UserBinaryDictionary)mUserDictionary).addWordToUserDictionary(word, 128);
- } else {
- ((UserDictionary)mUserDictionary).addWordToUserDictionary(word, 128);
- }
- // Suggestion strip should be updated after the operation of adding word to the
- // user dictionary
- mHandler.postUpdateSuggestions();
+ public boolean addWordToUserDictionary(final String word) {
+ mUserDictionary.addWordToUserDictionary(word, 128);
return true;
}
- private static boolean isAlphabet(int code) {
+ private static boolean isAlphabet(final int code) {
return Character.isLetter(code);
}
@@ -1170,7 +1202,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
public static final int CODE_SHOW_INPUT_METHOD_PICKER = 1;
@Override
- public boolean onCustomRequest(int requestCode) {
+ public boolean onCustomRequest(final int requestCode) {
if (isShowingOptionDialog()) return false;
switch (requestCode) {
case CODE_SHOW_INPUT_METHOD_PICKER:
@@ -1188,93 +1220,81 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
return mOptionsDialog != null && mOptionsDialog.isShowing();
}
- private static int getActionId(Keyboard keyboard) {
+ private static int getActionId(final Keyboard keyboard) {
return keyboard != null ? keyboard.mId.imeActionId() : EditorInfo.IME_ACTION_NONE;
}
- private void performEditorAction(int actionId) {
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- ic.performEditorAction(actionId);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_performEditorAction(actionId);
- }
- }
+ private void performEditorAction(final int actionId) {
+ mConnection.performEditorAction(actionId);
}
+ // TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
private void handleLanguageSwitchKey() {
- final boolean includesOtherImes = mSettingsValues.mIncludesOtherImesInLanguageSwitchList;
final IBinder token = getWindow().getWindow().getAttributes().token;
+ if (mCurrentSettings.mIncludesOtherImesInLanguageSwitchList) {
+ mImm.switchToNextInputMethod(token, false /* onlyCurrentIme */);
+ return;
+ }
if (mShouldSwitchToLastSubtype) {
final InputMethodSubtype lastSubtype = mImm.getLastInputMethodSubtype();
final boolean lastSubtypeBelongsToThisIme =
ImfUtils.checkIfSubtypeBelongsToThisImeAndEnabled(this, lastSubtype);
- if ((includesOtherImes || lastSubtypeBelongsToThisIme)
- && mImm.switchToLastInputMethod(token)) {
+ if (lastSubtypeBelongsToThisIme && mImm.switchToLastInputMethod(token)) {
mShouldSwitchToLastSubtype = false;
} else {
- mImm.switchToNextInputMethod(token, !includesOtherImes);
+ mImm.switchToNextInputMethod(token, true /* onlyCurrentIme */);
mShouldSwitchToLastSubtype = true;
}
} else {
- mImm.switchToNextInputMethod(token, !includesOtherImes);
+ mImm.switchToNextInputMethod(token, true /* onlyCurrentIme */);
}
}
- static private void sendUpDownEnterOrBackspace(final int code, final InputConnection ic) {
+ private void sendDownUpKeyEventForBackwardCompatibility(final int code) {
final long eventTime = SystemClock.uptimeMillis();
- ic.sendKeyEvent(new KeyEvent(eventTime, eventTime,
+ mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
- ic.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+ mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
}
- private void sendKeyCodePoint(int code) {
+ private void sendKeyCodePoint(final int code) {
// TODO: Remove this special handling of digit letters.
// For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
if (code >= '0' && code <= '9') {
- super.sendKeyChar((char)code);
- return;
- }
-
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
- // we want to be able to compile against the Ice Cream Sandwich SDK.
- if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null
- && mTargetApplicationInfo.targetSdkVersion < 16) {
- // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
- // a hardware keyboard event on pressing enter or delete. This is bad for many
- // reasons (there are race conditions with commits) but some applications are
- // relying on this behavior so we continue to support it for older apps.
- sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_ENTER, ic);
- } else {
- final String text = new String(new int[] { code }, 0, 1);
- ic.commitText(text, text.length());
- }
+ sendDownUpKeyEventForBackwardCompatibility(code - '0' + KeyEvent.KEYCODE_0);
if (ProductionFlag.IS_EXPERIMENTAL) {
ResearchLogger.latinIME_sendKeyCodePoint(code);
}
+ return;
+ }
+
+ // 16 is android.os.Build.VERSION_CODES.JELLY_BEAN but we can't write it because
+ // we want to be able to compile against the Ice Cream Sandwich SDK.
+ if (Keyboard.CODE_ENTER == code && mTargetApplicationInfo != null
+ && mTargetApplicationInfo.targetSdkVersion < 16) {
+ // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
+ // a hardware keyboard event on pressing enter or delete. This is bad for many
+ // reasons (there are race conditions with commits) but some applications are
+ // relying on this behavior so we continue to support it for older apps.
+ sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_ENTER);
+ } else {
+ final String text = new String(new int[] { code }, 0, 1);
+ mConnection.commitText(text, text.length());
}
}
// Implementation of {@link KeyboardActionListener}.
@Override
- public void onCodeInput(int primaryCode, int x, int y) {
+ public void onCodeInput(final int primaryCode, final int x, final int y) {
final long when = SystemClock.uptimeMillis();
if (primaryCode != Keyboard.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
mDeleteCount = 0;
}
mLastKeyTime = when;
-
- if (ProductionFlag.IS_EXPERIMENTAL) {
- if (ResearchLogger.sIsLogging) {
- ResearchLogger.getInstance().logKeyEvent(primaryCode, x, y);
- }
- }
-
+ mConnection.beginBatchEdit();
final KeyboardSwitcher switcher = mKeyboardSwitcher;
// The space state depends only on the last character pressed and its own previous
// state. Here, we revert the space state to neutral if the key is actually modifying
@@ -1307,7 +1327,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
onSettingsKeyPressed();
break;
case Keyboard.CODE_SHORTCUT:
- mSubtypeSwitcher.switchToShortcutIME();
+ mSubtypeSwitcher.switchToShortcutIME(this);
break;
case Keyboard.CODE_ACTION_ENTER:
performEditorAction(getActionId(switcher.getKeyboard()));
@@ -1321,23 +1341,35 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
case Keyboard.CODE_LANGUAGE_SWITCH:
handleLanguageSwitchKey();
break;
- default:
- if (primaryCode == Keyboard.CODE_TAB
- && mInputAttributes.mEditorAction == EditorInfo.IME_ACTION_NEXT) {
- performEditorAction(EditorInfo.IME_ACTION_NEXT);
- break;
+ case Keyboard.CODE_RESEARCH:
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.getInstance().onResearchKeySelected(this);
}
+ break;
+ default:
mSpaceState = SPACE_STATE_NONE;
- if (mSettingsValues.isWordSeparator(primaryCode)) {
+ if (mCurrentSettings.isWordSeparator(primaryCode)) {
didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
} else {
+ if (SPACE_STATE_PHANTOM == spaceState) {
+ if (ProductionFlag.IS_INTERNAL) {
+ if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
+ Stats.onAutoCorrection(
+ "", mWordComposer.getTypedWord(), " ", mWordComposer);
+ }
+ }
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ }
+ final int keyX, keyY;
final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
- handleCharacter(primaryCode, x, y, spaceState);
+ keyX = x;
+ keyY = y;
} else {
- handleCharacter(primaryCode, NOT_A_TOUCH_COORDINATE, NOT_A_TOUCH_COORDINATE,
- spaceState);
+ keyX = Constants.NOT_A_COORDINATE;
+ keyY = Constants.NOT_A_COORDINATE;
}
+ handleCharacter(primaryCode, keyX, keyY, spaceState);
}
mExpectingUpdateSelection = true;
mShouldSwitchToLastSubtype = true;
@@ -1348,34 +1380,201 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (!didAutoCorrect && primaryCode != Keyboard.CODE_SHIFT
&& primaryCode != Keyboard.CODE_SWITCH_ALPHA_SYMBOL)
mLastComposedWord.deactivate();
- mEnteredText = null;
+ if (Keyboard.CODE_DELETE != primaryCode) {
+ mEnteredText = null;
+ }
+ mConnection.endBatchEdit();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
+ }
}
+ // Called from PointerTracker through the KeyboardActionListener interface
@Override
- public void onTextInput(CharSequence text) {
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
- ic.beginBatchEdit();
- commitTyped(ic, LastComposedWord.NOT_A_SEPARATOR);
- text = specificTldProcessingOnTextInput(ic, text);
+ public void onTextInput(final CharSequence rawText) {
+ mConnection.beginBatchEdit();
+ if (mWordComposer.isComposingWord()) {
+ commitCurrentAutoCorrection(rawText.toString());
+ } else {
+ resetComposingState(true /* alsoResetLastComposedWord */);
+ }
+ mHandler.postUpdateSuggestionStrip();
+ final CharSequence text = specificTldProcessingOnTextInput(rawText);
if (SPACE_STATE_PHANTOM == mSpaceState) {
sendKeyCodePoint(Keyboard.CODE_SPACE);
}
- ic.commitText(text, 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitText(text);
- }
- ic.endBatchEdit();
+ mConnection.commitText(text, 1);
+ mConnection.endBatchEdit();
+ // Space state must be updated before calling updateShiftState
+ mSpaceState = SPACE_STATE_NONE;
mKeyboardSwitcher.updateShiftState();
mKeyboardSwitcher.onCodeInput(Keyboard.CODE_OUTPUT_TEXT);
- mSpaceState = SPACE_STATE_NONE;
mEnteredText = text;
- resetComposingState(true /* alsoResetLastComposedWord */);
}
- // ic may not be null
- private CharSequence specificTldProcessingOnTextInput(final InputConnection ic,
- final CharSequence text) {
+ @Override
+ public void onStartBatchInput() {
+ BatchInputUpdater.getInstance().onStartBatchInput();
+ mConnection.beginBatchEdit();
+ if (mWordComposer.isComposingWord()) {
+ if (ProductionFlag.IS_INTERNAL) {
+ if (mWordComposer.isBatchMode()) {
+ Stats.onAutoCorrection("", mWordComposer.getTypedWord(), " ", mWordComposer);
+ }
+ }
+ if (mWordComposer.size() <= 1) {
+ // We auto-correct the previous (typed, not gestured) string iff it's one character
+ // long. The reason for this is, even in the middle of gesture typing, you'll still
+ // tap one-letter words and you want them auto-corrected (typically, "i" in English
+ // should become "I"). However for any longer word, we assume that the reason for
+ // tapping probably is that the word you intend to type is not in the dictionary,
+ // so we do not attempt to correct, on the assumption that if that was a dictionary
+ // word, the user would probably have gestured instead.
+ commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR);
+ } else {
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ }
+ mExpectingUpdateSelection = true;
+ // The following is necessary for the case where the user typed something but didn't
+ // manual pick it and didn't input any separator.
+ mSpaceState = SPACE_STATE_PHANTOM;
+ } else {
+ final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
+ // TODO: reverse this logic. We should have the means to determine whether a character
+ // should usually be followed by a space, and it should be more readable.
+ if (Constants.NOT_A_CODE != codePointBeforeCursor
+ && !Character.isWhitespace(codePointBeforeCursor)
+ && !mCurrentSettings.isPhantomSpacePromotingSymbol(codePointBeforeCursor)
+ && !mCurrentSettings.isWeakSpaceStripper(codePointBeforeCursor)) {
+ mSpaceState = SPACE_STATE_PHANTOM;
+ }
+ }
+ mConnection.endBatchEdit();
+ mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+ }
+
+ private static final class BatchInputUpdater implements Handler.Callback {
+ private final Handler mHandler;
+ private LatinIME mLatinIme;
+ private boolean mInBatchInput; // synchornized using "this".
+
+ private BatchInputUpdater() {
+ final HandlerThread handlerThread = new HandlerThread(
+ BatchInputUpdater.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;
+ }
+
+ private static final int MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP = 1;
+
+ @Override
+ public boolean handleMessage(final Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
+ updateBatchInput((InputPointers)msg.obj, mLatinIme);
+ break;
+ }
+ return true;
+ }
+
+ // Run in the UI thread.
+ public synchronized void onStartBatchInput() {
+ mInBatchInput = true;
+ }
+
+ // Run in the Handler thread.
+ private synchronized void updateBatchInput(final InputPointers batchPointers,
+ final LatinIME latinIme) {
+ if (!mInBatchInput) {
+ // Batch input has ended while the message was being delivered.
+ return;
+ }
+ final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(
+ batchPointers, latinIme);
+ latinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+ suggestedWords, false /* dismissGestureFloatingPreviewText */);
+ }
+
+ // Run in the UI thread.
+ public void onUpdateBatchInput(final InputPointers batchPointers, final LatinIME latinIme) {
+ mLatinIme = latinIme;
+ if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) {
+ return;
+ }
+ mHandler.obtainMessage(
+ MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, batchPointers)
+ .sendToTarget();
+ }
+
+ // Run in the UI thread.
+ public synchronized SuggestedWords onEndBatchInput(final InputPointers batchPointers,
+ final LatinIME latinIme) {
+ mInBatchInput = false;
+ final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(
+ batchPointers, latinIme);
+ latinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+ suggestedWords, true /* dismissGestureFloatingPreviewText */);
+ return suggestedWords;
+ }
+
+ // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
+ // be synchronized.
+ private static SuggestedWords getSuggestedWordsGestureLocked(
+ final InputPointers batchPointers, final LatinIME latinIme) {
+ latinIme.mWordComposer.setBatchInputPointers(batchPointers);
+ return latinIme.getSuggestedWords(Suggest.SESSION_GESTURE);
+ }
+ }
+
+ private void showGesturePreviewAndSuggestionStrip(final SuggestedWords suggestedWords,
+ final boolean dismissGestureFloatingPreviewText) {
+ final String batchInputText = (suggestedWords.size() > 0)
+ ? suggestedWords.getWord(0) : null;
+ final KeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ mainKeyboardView.showGestureFloatingPreviewText(batchInputText);
+ showSuggestionStrip(suggestedWords, null);
+ if (dismissGestureFloatingPreviewText) {
+ mainKeyboardView.dismissGestureFloatingPreviewText();
+ }
+ }
+
+ @Override
+ public void onUpdateBatchInput(final InputPointers batchPointers) {
+ BatchInputUpdater.getInstance().onUpdateBatchInput(batchPointers, this);
+ }
+
+ @Override
+ public void onEndBatchInput(final InputPointers batchPointers) {
+ final SuggestedWords suggestedWords = BatchInputUpdater.getInstance().onEndBatchInput(
+ batchPointers, this);
+ final String batchInputText = (suggestedWords.size() > 0)
+ ? suggestedWords.getWord(0) : null;
+ if (TextUtils.isEmpty(batchInputText)) {
+ return;
+ }
+ mWordComposer.setBatchInputWord(batchInputText);
+ mConnection.beginBatchEdit();
+ if (SPACE_STATE_PHANTOM == mSpaceState) {
+ sendKeyCodePoint(Keyboard.CODE_SPACE);
+ }
+ mConnection.setComposingText(batchInputText, 1);
+ mExpectingUpdateSelection = true;
+ mConnection.endBatchEdit();
+ // Space state must be updated before calling updateShiftState
+ mSpaceState = SPACE_STATE_PHANTOM;
+ mKeyboardSwitcher.updateShiftState();
+ }
+
+ private CharSequence specificTldProcessingOnTextInput(final CharSequence text) {
if (text.length() <= 1 || text.charAt(0) != Keyboard.CODE_PERIOD
|| !Character.isLetter(text.charAt(1))) {
// Not a tld: do nothing.
@@ -1384,7 +1583,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// We have a TLD (or something that looks like this): make sure we don't add
// a space even if currently in phantom mode.
mSpaceState = SPACE_STATE_NONE;
- final CharSequence lastOne = ic.getTextBeforeCursor(1, 0);
+ // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code
+ final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
if (lastOne != null && lastOne.length() == 1
&& lastOne.charAt(0) == Keyboard.CODE_PERIOD) {
return text.subSequence(1, text.length());
@@ -1393,6 +1593,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
}
+ // Called from PointerTracker through the KeyboardActionListener interface
@Override
public void onCancelInput() {
// User released a finger outside any key
@@ -1400,67 +1601,54 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
private void handleBackspace(final int spaceState) {
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
- ic.beginBatchEdit();
- handleBackspaceWhileInBatchEdit(spaceState, ic);
- ic.endBatchEdit();
- }
-
- // "ic" may not be null.
- private void handleBackspaceWhileInBatchEdit(final int spaceState, final InputConnection ic) {
- // In many cases, we may have to put the keyboard in auto-shift state again.
+ // In many cases, we may have to put the keyboard in auto-shift state again. However
+ // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
+ // during key repeat.
mHandler.postUpdateShiftState();
- if (mEnteredText != null && sameAsTextBeforeCursor(ic, mEnteredText)) {
- // Cancel multi-character input: remove the text we just entered.
- // This is triggered on backspace after a key that inputs multiple characters,
- // like the smiley key or the .com key.
- final int length = mEnteredText.length();
- ic.deleteSurroundingText(length, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(length);
- }
- // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
- // In addition we know that spaceState is false, and that we should not be
- // reverting any autocorrect at this point. So we can safely return.
- return;
- }
-
if (mWordComposer.isComposingWord()) {
final int length = mWordComposer.size();
if (length > 0) {
- mWordComposer.deleteLast();
- ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
- // If we have deleted the last remaining character of a word, then we are not
- // isComposingWord() any more.
- if (!mWordComposer.isComposingWord()) {
- // Not composing word any more, so we can show bigrams.
- mHandler.postUpdateBigramPredictions();
+ // Immediately after a batch input.
+ if (SPACE_STATE_PHANTOM == spaceState) {
+ mWordComposer.reset();
} else {
- // Still composing a word, so we still have letters to deduce a suggestion from.
- mHandler.postUpdateSuggestions();
+ mWordComposer.deleteLast();
}
+ mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+ mHandler.postUpdateSuggestionStrip();
} else {
- ic.deleteSurroundingText(1, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
- }
+ mConnection.deleteSurroundingText(1, 0);
}
} else {
if (mLastComposedWord.canRevertCommit()) {
- Utils.Stats.onAutoCorrectionCancellation();
- revertCommit(ic);
+ if (ProductionFlag.IS_INTERNAL) {
+ Stats.onAutoCorrectionCancellation();
+ }
+ revertCommit();
+ return;
+ }
+ if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
+ // Cancel multi-character input: remove the text we just entered.
+ // This is triggered on backspace after a key that inputs multiple characters,
+ // like the smiley key or the .com key.
+ final int length = mEnteredText.length();
+ mConnection.deleteSurroundingText(length, 0);
+ mEnteredText = null;
+ // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
+ // In addition we know that spaceState is false, and that we should not be
+ // reverting any autocorrect at this point. So we can safely return.
return;
}
if (SPACE_STATE_DOUBLE == spaceState) {
- if (revertDoubleSpaceWhileInBatchEdit(ic)) {
+ mHandler.cancelDoubleSpacesTimer();
+ if (mConnection.revertDoubleSpace()) {
// No need to reset mSpaceState, it has already be done (that's why we
// receive it as a parameter)
return;
}
} else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
- if (revertSwapPunctuation(ic)) {
+ if (mConnection.revertSwapPunctuation()) {
// Likewise
return;
}
@@ -1471,11 +1659,8 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
if (mLastSelectionStart != mLastSelectionEnd) {
// If there is a selection, remove it.
final int lengthToDelete = mLastSelectionEnd - mLastSelectionStart;
- ic.setSelection(mLastSelectionEnd, mLastSelectionEnd);
- ic.deleteSurroundingText(lengthToDelete, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(lengthToDelete);
- }
+ mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+ mConnection.deleteSurroundingText(lengthToDelete, 0);
} else {
// There is no selection, just delete one character.
if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
@@ -1490,40 +1675,33 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// a hardware keyboard event on pressing enter or delete. This is bad for many
// reasons (there are race conditions with commits) but some applications are
// relying on this behavior so we continue to support it for older apps.
- sendUpDownEnterOrBackspace(KeyEvent.KEYCODE_DEL, ic);
+ sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL);
} else {
- ic.deleteSurroundingText(1, 0);
- }
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
+ mConnection.deleteSurroundingText(1, 0);
}
if (mDeleteCount > DELETE_ACCELERATE_AT) {
- ic.deleteSurroundingText(1, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(1);
- }
+ mConnection.deleteSurroundingText(1, 0);
}
}
- if (isSuggestionsRequested()) {
- restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(ic);
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+ restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
}
}
}
- // ic may be null
- private boolean maybeStripSpaceWhileInBatchEdit(final InputConnection ic, final int code,
+ private boolean maybeStripSpace(final int code,
final int spaceState, final boolean isFromSuggestionStrip) {
if (Keyboard.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
- removeTrailingSpaceWhileInBatchEdit(ic);
+ mConnection.removeTrailingSpace();
return false;
} else if ((SPACE_STATE_WEAK == spaceState
|| SPACE_STATE_SWAP_PUNCTUATION == spaceState)
&& isFromSuggestionStrip) {
- if (mSettingsValues.isWeakSpaceSwapper(code)) {
+ if (mCurrentSettings.isWeakSpaceSwapper(code)) {
return true;
} else {
- if (mSettingsValues.isWeakSpaceStripper(code)) {
- removeTrailingSpaceWhileInBatchEdit(ic);
+ if (mCurrentSettings.isWeakSpaceStripper(code)) {
+ mConnection.removeTrailingSpace();
}
return false;
}
@@ -1534,20 +1712,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private void handleCharacter(final int primaryCode, final int x,
final int y, final int spaceState) {
- final InputConnection ic = getCurrentInputConnection();
- if (null != ic) ic.beginBatchEdit();
- // TODO: if ic is null, does it make any sense to call this?
- handleCharacterWhileInBatchEdit(primaryCode, x, y, spaceState, ic);
- if (null != ic) ic.endBatchEdit();
- }
-
- // "ic" may be null without this crashing, but the behavior will be really strange
- private void handleCharacterWhileInBatchEdit(final int primaryCode,
- final int x, final int y, final int spaceState, final InputConnection ic) {
boolean isComposingWord = mWordComposer.isComposingWord();
if (SPACE_STATE_PHANTOM == spaceState &&
- !mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode)) {
+ !mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode)) {
if (isComposingWord) {
// Sanity check
throw new RuntimeException("Should not be composing here");
@@ -1559,8 +1727,9 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// dozen milliseconds. Avoid calling it as much as possible, since we are on the UI
// thread here.
if (!isComposingWord && (isAlphabet(primaryCode)
- || mSettingsValues.isSymbolExcludedFromWordSeparators(primaryCode))
- && isSuggestionsRequested() && !isCursorTouchingWord()) {
+ || mCurrentSettings.isSymbolExcludedFromWordSeparators(primaryCode))
+ && mCurrentSettings.isSuggestionsRequested(mDisplayOrientation) &&
+ !mConnection.isCursorTouchingWord(mCurrentSettings)) {
// 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
@@ -1571,85 +1740,71 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// it entirely and resume suggestions on the previous word, we'd like to still
// have touch coordinates for it.
resetComposingState(false /* alsoResetLastComposedWord */);
- clearSuggestions();
}
if (isComposingWord) {
- mWordComposer.add(
- primaryCode, x, y, mKeyboardSwitcher.getKeyboardView().getKeyDetector());
- if (ic != null) {
- // If it's the first letter, make note of auto-caps state
- if (mWordComposer.size() == 1) {
- mWordComposer.setAutoCapitalized(
- getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
- }
- ic.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+ final int keyX, keyY;
+ if (KeyboardActionListener.Adapter.isInvalidCoordinate(x)
+ || KeyboardActionListener.Adapter.isInvalidCoordinate(y)) {
+ keyX = x;
+ keyY = y;
+ } else {
+ final KeyDetector keyDetector =
+ mKeyboardSwitcher.getMainKeyboardView().getKeyDetector();
+ keyX = keyDetector.getTouchX(x);
+ keyY = keyDetector.getTouchY(y);
+ }
+ mWordComposer.add(primaryCode, keyX, keyY);
+ // If it's the first letter, make note of auto-caps state
+ if (mWordComposer.size() == 1) {
+ mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
}
- mHandler.postUpdateSuggestions();
+ mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
} else {
- final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode,
- spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
+ final boolean swapWeakSpace = maybeStripSpace(primaryCode,
+ spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
sendKeyCodePoint(primaryCode);
if (swapWeakSpace) {
- swapSwapperAndSpaceWhileInBatchEdit(ic);
+ swapSwapperAndSpace();
mSpaceState = SPACE_STATE_WEAK;
}
- // Some characters are not word separators, yet they don't start a new
- // composing span. For these, we haven't changed the suggestion strip, and
- // if the "add to dictionary" hint is shown, we should do so now. Examples of
- // such characters include single quote, dollar, and others; the exact list is
- // the list of characters for which we enter handleCharacterWhileInBatchEdit
- // that don't match the test if ((isAlphabet...)) at the top of this method.
- if (null != mSuggestionsView && mSuggestionsView.dismissAddToDictionaryHint()) {
- mHandler.postUpdateBigramPredictions();
- }
+ // In case the "add to dictionary" hint was still displayed.
+ if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint();
+ }
+ mHandler.postUpdateSuggestionStrip();
+ if (ProductionFlag.IS_INTERNAL) {
+ Utils.Stats.onNonSeparator((char)primaryCode, x, y);
}
- Utils.Stats.onNonSeparator((char)primaryCode, x, y);
}
// Returns true if we did an autocorrection, false otherwise.
private boolean handleSeparator(final int primaryCode, final int x, final int y,
final int spaceState) {
- // Should dismiss the "Touch again to save" message when handling separator
- if (mSuggestionsView != null && mSuggestionsView.dismissAddToDictionaryHint()) {
- mHandler.cancelUpdateBigramPredictions();
- mHandler.postUpdateSuggestions();
- }
-
boolean didAutoCorrect = false;
// Handle separator
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- ic.beginBatchEdit();
- }
if (mWordComposer.isComposingWord()) {
- // In certain languages where single quote is a separator, it's better
- // not to auto correct, but accept the typed word. For instance,
- // in Italian dov' should not be expanded to dove' because the elision
- // requires the last vowel to be removed.
- final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
- && !mInputAttributes.mInputTypeNoAutoCorrect;
- if (shouldAutoCorrect && primaryCode != Keyboard.CODE_SINGLE_QUOTE) {
- commitCurrentAutoCorrection(primaryCode, ic);
+ if (mCurrentSettings.mCorrectionEnabled) {
+ // TODO: maybe cache Strings in an <String> sparse array or something
+ commitCurrentAutoCorrection(new String(new int[]{primaryCode}, 0, 1));
didAutoCorrect = true;
} else {
- commitTyped(ic, primaryCode);
+ commitTyped(new String(new int[]{primaryCode}, 0, 1));
}
}
- final boolean swapWeakSpace = maybeStripSpaceWhileInBatchEdit(ic, primaryCode, spaceState,
- KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
+ final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
+ Constants.SUGGESTION_STRIP_COORDINATE == x);
if (SPACE_STATE_PHANTOM == spaceState &&
- mSettingsValues.isPhantomSpacePromotingSymbol(primaryCode)) {
+ mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) {
sendKeyCodePoint(Keyboard.CODE_SPACE);
}
sendKeyCodePoint(primaryCode);
if (Keyboard.CODE_SPACE == primaryCode) {
- if (isSuggestionsRequested()) {
- if (maybeDoubleSpaceWhileInBatchEdit(ic)) {
+ if (mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
+ if (maybeDoubleSpace()) {
mSpaceState = SPACE_STATE_DOUBLE;
} else if (!isShowingPunctuationList()) {
mSpaceState = SPACE_STATE_WEAK;
@@ -1657,21 +1812,26 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
mHandler.startDoubleSpacesTimer();
- if (!isCursorTouchingWord()) {
- mHandler.cancelUpdateSuggestions();
- mHandler.postUpdateBigramPredictions();
+ if (!mConnection.isCursorTouchingWord(mCurrentSettings)) {
+ mHandler.postUpdateSuggestionStrip();
}
} else {
if (swapWeakSpace) {
- swapSwapperAndSpaceWhileInBatchEdit(ic);
+ swapSwapperAndSpace();
mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
- } else if (SPACE_STATE_PHANTOM == spaceState) {
+ } else if (SPACE_STATE_PHANTOM == spaceState
+ && !mCurrentSettings.isWeakSpaceStripper(primaryCode)
+ && !mCurrentSettings.isPhantomSpacePromotingSymbol(primaryCode)) {
// If we are in phantom space state, and the user presses a separator, we want to
// stay in phantom space state so that the next keypress has a chance to add the
// space. For example, if I type "Good dat", pick "day" from the suggestion strip
// then insert a comma and go on to typing the next word, I want the space to be
// inserted automatically before the next word, the same way it is when I don't
// input the comma.
+ // The case is a little different if the separator is a space stripper. Such a
+ // separator does not normally need a space on the right (that's the difference
+ // between swappers and strippers), so we should not stay in phantom space state if
+ // the separator is a stripper. Hence the additional test above.
mSpaceState = SPACE_STATE_PHANTOM;
}
@@ -1679,12 +1839,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// already displayed or not, so it's okay.
setPunctuationSuggestions();
}
-
- Utils.Stats.onSeparator((char)primaryCode, x, y);
-
- if (ic != null) {
- ic.endBatchEdit();
+ if (ProductionFlag.IS_INTERNAL) {
+ Utils.Stats.onSeparator((char)primaryCode, x, y);
}
+
+ mKeyboardSwitcher.updateShiftState();
return didAutoCorrect;
}
@@ -1695,153 +1854,142 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
private void handleClose() {
- commitTyped(getCurrentInputConnection(), LastComposedWord.NOT_A_SEPARATOR);
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
requestHideSelf(0);
- LatinKeyboardView inputView = mKeyboardSwitcher.getKeyboardView();
- if (inputView != null)
- inputView.closing();
- }
-
- public boolean isSuggestionsRequested() {
- return mInputAttributes.mIsSettingsSuggestionStripOn
- && (mCorrectionMode > 0 || isShowingSuggestionsStrip());
- }
-
- public boolean isShowingPunctuationList() {
- if (mSuggestionsView == null) return false;
- return mSettingsValues.mSuggestPuncList == mSuggestionsView.getSuggestions();
+ final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+ if (mainKeyboardView != null) {
+ mainKeyboardView.closing();
+ }
}
- public boolean isShowingSuggestionsStrip() {
- return (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_VALUE)
- || (mSuggestionVisibility == SUGGESTION_VISIBILILTY_SHOW_ONLY_PORTRAIT_VALUE
- && mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT);
+ // TODO: make this private
+ // Outside LatinIME, only used by the test suite.
+ /* package for tests */
+ boolean isShowingPunctuationList() {
+ if (mSuggestionStripView == null) return false;
+ return mCurrentSettings.mSuggestPuncList == mSuggestionStripView.getSuggestions();
}
- public boolean isSuggestionsStripVisible() {
- if (mSuggestionsView == null)
+ private boolean isSuggestionsStripVisible() {
+ if (mSuggestionStripView == null)
return false;
- if (mSuggestionsView.isShowingAddToDictionaryHint())
+ if (mSuggestionStripView.isShowingAddToDictionaryHint())
return true;
- if (!isShowingSuggestionsStrip())
+ if (!mCurrentSettings.isSuggestionStripVisibleInOrientation(mDisplayOrientation))
return false;
- if (mInputAttributes.mApplicationSpecifiedCompletionOn)
+ if (mCurrentSettings.isApplicationSpecifiedCompletionsOn())
return true;
- return isSuggestionsRequested();
- }
-
- public void switchToKeyboardView() {
- if (DEBUG) {
- Log.d(TAG, "Switch to keyboard view.");
- }
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_switchToKeyboardView();
- }
- View v = mKeyboardSwitcher.getKeyboardView();
- if (v != null) {
- // Confirms that the keyboard view doesn't have parent view.
- ViewParent p = v.getParent();
- if (p != null && p instanceof ViewGroup) {
- ((ViewGroup) p).removeView(v);
- }
- setInputView(v);
- }
- setSuggestionStripShown(isSuggestionsStripVisible());
- updateInputViewShown();
- mHandler.postUpdateSuggestions();
+ return mCurrentSettings.isSuggestionsRequested(mDisplayOrientation);
}
- public void clearSuggestions() {
- setSuggestions(SuggestedWords.EMPTY, false);
+ private void clearSuggestionStrip() {
+ setSuggestionStrip(SuggestedWords.EMPTY, false);
setAutoCorrectionIndicator(false);
}
- private void setSuggestions(final SuggestedWords words, final boolean isAutoCorrection) {
- if (mSuggestionsView != null) {
- mSuggestionsView.setSuggestions(words);
+ private void setSuggestionStrip(final SuggestedWords words, final boolean isAutoCorrection) {
+ if (mSuggestionStripView != null) {
+ mSuggestionStripView.setSuggestions(words);
mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
}
}
private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
// Put a blue underline to a word in TextView which will be auto-corrected.
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return;
if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
&& mWordComposer.isComposingWord()) {
mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
final CharSequence textWithUnderline =
getTextWithUnderline(mWordComposer.getTypedWord());
- ic.setComposingText(textWithUnderline, 1);
+ // TODO: when called from an updateSuggestionStrip() call that results from a posted
+ // message, this is called outside any batch edit. Potentially, this may result in some
+ // janky flickering of the screen, although the display speed makes it unlikely in
+ // the practice.
+ mConnection.setComposingText(textWithUnderline, 1);
}
}
- public void updateSuggestions() {
+ private void updateSuggestionStrip() {
+ mHandler.cancelUpdateSuggestionStrip();
+
// Check if we have a suggestion engine attached.
- if ((mSuggest == null || !isSuggestionsRequested())) {
+ if (mSuggest == null || !mCurrentSettings.isSuggestionsRequested(mDisplayOrientation)) {
if (mWordComposer.isComposingWord()) {
- Log.w(TAG, "Called updateSuggestions but suggestions were not requested!");
+ Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
+ + "requested!");
mWordComposer.setAutoCorrection(mWordComposer.getTypedWord());
}
return;
}
- mHandler.cancelUpdateSuggestions();
- mHandler.cancelUpdateBigramPredictions();
-
- if (!mWordComposer.isComposingWord()) {
+ if (!mWordComposer.isComposingWord() && !mCurrentSettings.mBigramPredictionEnabled) {
setPunctuationSuggestions();
return;
}
- // TODO: May need a better way of retrieving previous word
- final InputConnection ic = getCurrentInputConnection();
- final CharSequence prevWord;
- if (null == ic) {
- prevWord = null;
- } else {
- prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
- }
+ final SuggestedWords suggestedWords = getSuggestedWords(Suggest.SESSION_TYPING);
+ final String typedWord = mWordComposer.getTypedWord();
+ showSuggestionStrip(suggestedWords, typedWord);
+ }
- final CharSequence typedWord = mWordComposer.getTypedWord();
- // getSuggestedWords handles gracefully a null value of prevWord
+ private SuggestedWords getSuggestedWords(final int sessionId) {
+ final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
+ if (keyboard == null) {
+ return SuggestedWords.EMPTY;
+ }
+ final String typedWord = mWordComposer.getTypedWord();
+ // 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 CharSequence prevWord =
+ mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators,
+ mWordComposer.isComposingWord() ? 2 : 1);
final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
- prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(), mCorrectionMode);
-
- // Basically, we update the suggestion strip only when suggestion count > 1. However,
- // there is an exception: We update the suggestion strip whenever typed word's length
- // is 1 or typed word is found in dictionary, regardless of suggestion count. Actually,
- // in most cases, suggestion count is 1 when typed word's length is 1, but we do always
- // need to clear the previous state when the user starts typing a word (i.e. typed word's
- // length == 1).
- if (suggestedWords.size() > 1 || typedWord.length() == 1
- || !suggestedWords.mAllowsToBeAutoCorrected
- || mSuggestionsView.isShowingAddToDictionaryHint()) {
- showSuggestions(suggestedWords, typedWord);
+ prevWord, keyboard.getProximityInfo(), mCurrentSettings.mCorrectionEnabled,
+ sessionId);
+ return maybeRetrieveOlderSuggestions(typedWord, suggestedWords);
+ }
+
+ private SuggestedWords maybeRetrieveOlderSuggestions(final CharSequence typedWord,
+ final SuggestedWords suggestedWords) {
+ // TODO: consolidate this into getSuggestedWords
+ // We update the suggestion strip only when we have some suggestions to show, i.e. when
+ // the suggestion count is > 1; else, we leave the old suggestions, with the typed word
+ // replaced with the new one. However, when the word is a dictionary word, or when the
+ // length of the typed word is 1 or 0 (after a deletion typically), we do want to remove the
+ // old suggestions. Also, if we are showing the "add to dictionary" hint, we need to
+ // revert to suggestions - although it is unclear how we can come here if it's displayed.
+ if (suggestedWords.size() > 1 || typedWord.length() <= 1
+ || !suggestedWords.mTypedWordValid
+ || mSuggestionStripView.isShowingAddToDictionaryHint()) {
+ return suggestedWords;
} else {
- SuggestedWords previousSuggestions = mSuggestionsView.getSuggestions();
- if (previousSuggestions == mSettingsValues.mSuggestPuncList) {
+ SuggestedWords previousSuggestions = mSuggestionStripView.getSuggestions();
+ if (previousSuggestions == mCurrentSettings.mSuggestPuncList) {
previousSuggestions = SuggestedWords.EMPTY;
}
final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
SuggestedWords.getTypedWordAndPreviousSuggestions(
typedWord, previousSuggestions);
- final SuggestedWords obsoleteSuggestedWords =
- new SuggestedWords(typedWordAndPreviousSuggestions,
+ return new SuggestedWords(typedWordAndPreviousSuggestions,
false /* typedWordValid */,
false /* hasAutoCorrectionCandidate */,
- false /* allowsToBeAutoCorrected */,
false /* isPunctuationSuggestions */,
true /* isObsoleteSuggestions */,
false /* isPrediction */);
- showSuggestions(obsoleteSuggestedWords, typedWord);
}
}
- public void showSuggestions(final SuggestedWords suggestedWords, final CharSequence typedWord) {
+ private void showSuggestionStrip(final SuggestedWords suggestedWords,
+ final CharSequence typedWord) {
+ if (null == suggestedWords || suggestedWords.size() <= 0) {
+ clearSuggestionStrip();
+ return;
+ }
final CharSequence autoCorrection;
if (suggestedWords.size() > 0) {
- if (suggestedWords.hasAutoCorrectionWord()) {
+ if (suggestedWords.mWillAutoCorrect) {
autoCorrection = suggestedWords.getWord(1);
} else {
autoCorrection = typedWord;
@@ -1851,94 +1999,89 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
mWordComposer.setAutoCorrection(autoCorrection);
final boolean isAutoCorrection = suggestedWords.willAutoCorrect();
- setSuggestions(suggestedWords, isAutoCorrection);
+ setSuggestionStrip(suggestedWords, isAutoCorrection);
setAutoCorrectionIndicator(isAutoCorrection);
setSuggestionStripShown(isSuggestionsStripVisible());
}
- private void commitCurrentAutoCorrection(final int separatorCodePoint,
- final InputConnection ic) {
+ private void commitCurrentAutoCorrection(final String separatorString) {
// Complete any pending suggestions query first
if (mHandler.hasPendingUpdateSuggestions()) {
- mHandler.cancelUpdateSuggestions();
- updateSuggestions();
+ updateSuggestionStrip();
}
- final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull();
+ final CharSequence typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
+ final String typedWord = mWordComposer.getTypedWord();
+ final CharSequence autoCorrection = (typedAutoCorrection != null)
+ ? typedAutoCorrection : typedWord;
if (autoCorrection != null) {
- final String typedWord = mWordComposer.getTypedWord();
if (TextUtils.isEmpty(typedWord)) {
throw new RuntimeException("We have an auto-correction but the typed word "
+ "is empty? Impossible! I must commit suicide.");
}
- Utils.Stats.onAutoCorrection(typedWord, autoCorrection.toString(), separatorCodePoint);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitCurrentAutoCorrection(typedWord,
- autoCorrection.toString());
+ if (ProductionFlag.IS_INTERNAL) {
+ Stats.onAutoCorrection(
+ typedWord, autoCorrection.toString(), separatorString, mWordComposer);
}
mExpectingUpdateSelection = true;
commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
- separatorCodePoint);
- if (!typedWord.equals(autoCorrection) && null != ic) {
+ separatorString);
+ 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.
- ic.commitCorrection(new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
+ // to the user that auto-correction happened. It has no other effect; in particular
+ // note that this won't affect the text inside the text field AT ALL: it only makes
+ // the segment of text starting at the supplied index and running for the length
+ // of the auto-correction flash. At this moment, the "typedWord" argument is
+ // ignored by TextView.
+ mConnection.commitCorrection(
+ new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
typedWord, autoCorrection));
}
}
}
+ // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
+ // interface
@Override
- public void pickSuggestionManually(final int index, final CharSequence suggestion,
- int x, int y) {
- final InputConnection ic = getCurrentInputConnection();
- if (null != ic) ic.beginBatchEdit();
- pickSuggestionManuallyWhileInBatchEdit(index, suggestion, x, y, ic);
- if (null != ic) ic.endBatchEdit();
- }
-
- public void pickSuggestionManuallyWhileInBatchEdit(final int index,
- final CharSequence suggestion, final int x, final int y, final InputConnection ic) {
- final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
+ public void pickSuggestionManually(final int index, final CharSequence suggestion) {
+ final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions();
// If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
if (suggestion.length() == 1 && isShowingPunctuationList()) {
// Word separators are suggested before the user inputs something.
// So, LatinImeLogger logs "" as a user's input.
LatinImeLogger.logOnManualSuggestion("", suggestion.toString(), index, suggestedWords);
// Rely on onCodeInput to do the complicated swapping/stripping logic consistently.
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, x, y);
- }
final int primaryCode = suggestion.charAt(0);
onCodeInput(primaryCode,
- KeyboardActionListener.SUGGESTION_STRIP_COORDINATE,
- KeyboardActionListener.SUGGESTION_STRIP_COORDINATE);
+ Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_punctuationSuggestion(index, suggestion);
+ }
return;
}
- if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0) {
+ mConnection.beginBatchEdit();
+ 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()) {
int firstChar = Character.codePointAt(suggestion, 0);
- if ((!mSettingsValues.isWeakSpaceStripper(firstChar))
- && (!mSettingsValues.isWeakSpaceSwapper(firstChar))) {
+ if ((!mCurrentSettings.isWeakSpaceStripper(firstChar))
+ && (!mCurrentSettings.isWeakSpaceSwapper(firstChar))) {
sendKeyCodePoint(Keyboard.CODE_SPACE);
}
}
- if (mInputAttributes.mApplicationSpecifiedCompletionOn
+ if (mCurrentSettings.isApplicationSpecifiedCompletionsOn()
&& mApplicationSpecifiedCompletions != null
&& index >= 0 && index < mApplicationSpecifiedCompletions.length) {
- if (mSuggestionsView != null) {
- mSuggestionsView.clear();
+ if (mSuggestionStripView != null) {
+ mSuggestionStripView.clear();
}
mKeyboardSwitcher.updateShiftState();
resetComposingState(true /* alsoResetLastComposedWord */);
- if (ic != null) {
- final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
- ic.commitCompletion(completionInfo);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_pickApplicationSpecifiedCompletion(index,
- completionInfo.getText(), x, y);
- }
- }
+ final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
+ mConnection.commitCompletion(completionInfo);
+ mConnection.endBatchEdit();
return;
}
@@ -1947,50 +2090,37 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final String replacedWord = mWordComposer.getTypedWord().toString();
LatinImeLogger.logOnManualSuggestion(replacedWord,
suggestion.toString(), index, suggestedWords);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, x, y);
- }
mExpectingUpdateSelection = true;
commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
LastComposedWord.NOT_A_SEPARATOR);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion);
+ }
+ mConnection.endBatchEdit();
// Don't allow cancellation of manual pick
mLastComposedWord.deactivate();
+ // Space state must be updated before calling updateShiftState
mSpaceState = SPACE_STATE_PHANTOM;
- // TODO: is this necessary?
mKeyboardSwitcher.updateShiftState();
// We should show the "Touch again to save" hint if the user pressed the first entry
- // AND either:
- // - There is no dictionary (we know that because we tried to load it => null != mSuggest
- // AND mSuggest.hasMainDictionary() is false)
- // - There is a dictionary and the word is not in it
+ // 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
- // We used to look at mCorrectionMode here, but showing the hint should have nothing
- // to do with the autocorrection setting.
final boolean showingAddToDictionaryHint = index == 0 && mSuggest != null
- // If there is no dictionary the hint should be shown.
- && (!mSuggest.hasMainDictionary()
- // If "suggestion" is not in the dictionary, the hint should be shown.
- || !AutoCorrection.isValidWord(
- mSuggest.getUnigramDictionaries(), suggestion, true));
-
- Utils.Stats.onSeparator((char)Keyboard.CODE_SPACE, WordComposer.NOT_A_COORDINATE,
- WordComposer.NOT_A_COORDINATE);
- if (!showingAddToDictionaryHint) {
- // If we're not showing the "Touch again to save", then show corrections again.
- // In case the cursor position doesn't change, make sure we show the suggestions again.
- updateBigramPredictions();
- // Updating the predictions right away may be slow and feel unresponsive on slower
- // terminals. On the other hand if we just postUpdateBigramPredictions() it will
- // take a noticeable delay to update them which may feel uneasy.
+ // If the suggestion is not in the dictionary, the hint should be shown.
+ && !AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(), suggestion, true);
+
+ if (ProductionFlag.IS_INTERNAL) {
+ Stats.onSeparator((char)Keyboard.CODE_SPACE,
+ Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+ }
+ if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
+ mSuggestionStripView.showAddToDictionaryHint(
+ suggestion, mCurrentSettings.mHintToSaveText);
} else {
- if (mIsUserDictionaryAvailable) {
- mSuggestionsView.showAddToDictionaryHint(
- suggestion, mSettingsValues.mHintToSaveText);
- } else {
- mHandler.postUpdateSuggestions();
- }
+ // If we're not showing the "Touch again to save", then update the suggestion strip.
+ mHandler.postUpdateSuggestionStrip();
}
}
@@ -1998,24 +2128,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
* Commits the chosen word to the text field and saves it for later retrieval.
*/
private void commitChosenWord(final CharSequence chosenWord, final int commitType,
- final int separatorCode) {
- final InputConnection ic = getCurrentInputConnection();
- if (ic != null) {
- if (mSettingsValues.mEnableSuggestionSpanInsertion) {
- final SuggestedWords suggestedWords = mSuggestionsView.getSuggestions();
- ic.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
- this, chosenWord, suggestedWords, mIsMainDictionaryAvailable),
- 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitText(chosenWord);
- }
- } else {
- ic.commitText(chosenWord, 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_commitText(chosenWord);
- }
- }
- }
+ final String separatorString) {
+ final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions();
+ mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
+ this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
// Add the word to the user history dictionary
final CharSequence prevWord = addToUserHistoryDictionary(chosenWord);
// TODO: figure out here if this is an auto-correct or if the best word is actually
@@ -2023,45 +2139,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// LastComposedWord#didCommitTypedWord by string equality of the remembered
// strings.
mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord.toString(),
- separatorCode, prevWord);
- }
-
- public void updateBigramPredictions() {
- if (mSuggest == null || !isSuggestionsRequested())
- return;
-
- if (!mSettingsValues.mBigramPredictionEnabled) {
- setPunctuationSuggestions();
- return;
- }
-
- final SuggestedWords suggestedWords;
- if (mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM) {
- final CharSequence prevWord = EditingUtils.getThisWord(getCurrentInputConnection(),
- mSettingsValues.mWordSeparators);
- if (!TextUtils.isEmpty(prevWord)) {
- suggestedWords = mSuggest.getBigramPredictions(prevWord);
- } else {
- suggestedWords = null;
- }
- } else {
- suggestedWords = null;
- }
-
- if (null != suggestedWords && suggestedWords.size() > 0) {
- // Explicitly supply an empty typed word (the no-second-arg version of
- // showSuggestions will retrieve the word near the cursor, we don't want that here)
- showSuggestions(suggestedWords, "");
- } else {
- clearSuggestions();
- }
+ separatorString, prevWord);
}
- public void setPunctuationSuggestions() {
- if (mSettingsValues.mBigramPredictionEnabled) {
- clearSuggestions();
+ private void setPunctuationSuggestions() {
+ if (mCurrentSettings.mBigramPredictionEnabled) {
+ clearSuggestionStrip();
} else {
- setSuggestions(mSettingsValues.mSuggestPuncList, false);
+ setSuggestionStrip(mCurrentSettings.mSuggestPuncList, false);
}
setAutoCorrectionIndicator(false);
setSuggestionStripShown(isSuggestionsStripVisible());
@@ -2069,25 +2154,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
private CharSequence addToUserHistoryDictionary(final CharSequence suggestion) {
if (TextUtils.isEmpty(suggestion)) return null;
+ if (mSuggest == null) return null;
- // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
- // adding words in situations where the user or application really didn't
- // want corrections enabled or learned.
- if (!(mCorrectionMode == Suggest.CORRECTION_FULL
- || mCorrectionMode == Suggest.CORRECTION_FULL_BIGRAM)) {
- 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 (!mCurrentSettings.mCorrectionEnabled) return null;
- if (mUserHistoryDictionary != null) {
- final InputConnection ic = getCurrentInputConnection();
- final CharSequence prevWord;
- if (null != ic) {
- prevWord = EditingUtils.getPreviousWord(ic, mSettingsValues.mWordSeparators);
- } else {
- prevWord = null;
- }
+ final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
+ if (userHistoryDictionary != null) {
+ final CharSequence prevWord
+ = mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 2);
final String secondWord;
- if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
+ if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
secondWord = suggestion.toString().toLowerCase(
mSubtypeSwitcher.getCurrentSubtypeLocale());
} else {
@@ -2098,101 +2177,39 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final int maxFreq = AutoCorrection.getMaxFrequency(
mSuggest.getUnigramDictionaries(), suggestion);
if (maxFreq == 0) return null;
- mUserHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
+ userHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
secondWord, maxFreq > 0);
return prevWord;
}
return null;
}
- public boolean isCursorTouchingWord() {
- final InputConnection ic = getCurrentInputConnection();
- if (ic == null) return false;
- CharSequence before = ic.getTextBeforeCursor(1, 0);
- CharSequence after = ic.getTextAfterCursor(1, 0);
- if (!TextUtils.isEmpty(before) && !mSettingsValues.isWordSeparator(before.charAt(0))
- && !mSettingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
- return true;
- }
- if (!TextUtils.isEmpty(after) && !mSettingsValues.isWordSeparator(after.charAt(0))
- && !mSettingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
- return true;
- }
- return false;
- }
-
- // "ic" must not be null
- private static boolean sameAsTextBeforeCursor(final InputConnection ic,
- final CharSequence text) {
- final CharSequence beforeText = ic.getTextBeforeCursor(text.length(), 0);
- return TextUtils.equals(text, beforeText);
- }
-
- // "ic" must not be null
/**
* Check if the cursor is actually at the end of a word. If so, restart suggestions on this
* word, else do nothing.
*/
- private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(
- final InputConnection ic) {
- // Bail out if the cursor is not at the end of a word (cursor must be preceded by
- // non-whitespace, non-separator, non-start-of-text)
- // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
- final CharSequence textBeforeCursor = ic.getTextBeforeCursor(1, 0);
- if (TextUtils.isEmpty(textBeforeCursor)
- || mSettingsValues.isWordSeparator(textBeforeCursor.charAt(0))) return;
-
- // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
- // separator or end of line/text)
- // Example: "test|"<EOL> "te|st" get rejected here
- final CharSequence textAfterCursor = ic.getTextAfterCursor(1, 0);
- if (!TextUtils.isEmpty(textAfterCursor)
- && !mSettingsValues.isWordSeparator(textAfterCursor.charAt(0))) return;
-
- // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
- // Example: " -|" gets rejected here but "e-|" and "e|" are okay
- CharSequence word = EditingUtils.getWordAtCursor(ic, mSettingsValues.mWordSeparators);
- // We don't suggest on leading single quotes, so we have to remove them from the word if
- // it starts with single quotes.
- while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
- word = word.subSequence(1, word.length());
- }
- if (TextUtils.isEmpty(word)) return;
- final char firstChar = word.charAt(0); // we just tested that word is not empty
- if (word.length() == 1 && !Character.isLetter(firstChar)) return;
-
- // We only suggest on words that start with a letter or a symbol that is excluded from
- // word separators (see #handleCharacterWhileInBatchEdit).
- if (!(isAlphabet(firstChar)
- || mSettingsValues.isSymbolExcludedFromWordSeparators(firstChar))) {
- return;
+ private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
+ final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(mCurrentSettings);
+ if (null != word) {
+ restartSuggestionsOnWordBeforeCursor(word);
}
-
- // Okay, we are at the end of a word. Restart suggestions.
- restartSuggestionsOnWordBeforeCursor(ic, word);
}
- // "ic" must not be null
- private void restartSuggestionsOnWordBeforeCursor(final InputConnection ic,
- final CharSequence word) {
+ private void restartSuggestionsOnWordBeforeCursor(final CharSequence word) {
mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
final int length = word.length();
- ic.deleteSurroundingText(length, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(length);
- }
- ic.setComposingText(word, 1);
- mHandler.postUpdateSuggestions();
+ mConnection.deleteSurroundingText(length, 0);
+ mConnection.setComposingText(word, 1);
+ mHandler.postUpdateSuggestionStrip();
}
- // "ic" must not be null
- private void revertCommit(final InputConnection ic) {
+ private void revertCommit() {
final CharSequence previousWord = mLastComposedWord.mPrevWord;
final String originallyTypedWord = mLastComposedWord.mTypedWord;
final CharSequence committedWord = mLastComposedWord.mCommittedWord;
final int cancelLength = committedWord.length();
final int separatorLength = LastComposedWord.getSeparatorLength(
- mLastComposedWord.mSeparatorCode);
+ mLastComposedWord.mSeparatorString);
// TODO: should we check our saved separator against the actual contents of the text view?
final int deleteLength = cancelLength + separatorLength;
if (DEBUG) {
@@ -2200,7 +2217,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
throw new RuntimeException("revertCommit, but we are composing a word");
}
final String wordBeforeCursor =
- ic.getTextBeforeCursor(deleteLength, 0)
+ mConnection.getTextBeforeCursor(deleteLength, 0)
.subSequence(0, cancelLength).toString();
if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
throw new RuntimeException("revertCommit check failed: we thought we were "
@@ -2208,132 +2225,67 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
+ "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
}
}
- ic.deleteSurroundingText(deleteLength, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(deleteLength);
- }
+ mConnection.deleteSurroundingText(deleteLength, 0);
if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
mUserHistoryDictionary.cancelAddingUserHistory(
previousWord.toString(), committedWord.toString());
}
- if (0 == separatorLength || mLastComposedWord.didCommitTypedWord()) {
- // This is the case when we cancel a manual pick.
- // We should restart suggestion on the word right away.
- mWordComposer.resumeSuggestionOnLastComposedWord(mLastComposedWord);
- ic.setComposingText(originallyTypedWord, 1);
- } else {
- ic.commitText(originallyTypedWord, 1);
- // Re-insert the separator
- sendKeyCodePoint(mLastComposedWord.mSeparatorCode);
- Utils.Stats.onSeparator(mLastComposedWord.mSeparatorCode, WordComposer.NOT_A_COORDINATE,
- WordComposer.NOT_A_COORDINATE);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_revertCommit(originallyTypedWord);
- }
- // Don't restart suggestion yet. We'll restart if the user deletes the
- // separator.
+ mConnection.commitText(originallyTypedWord + mLastComposedWord.mSeparatorString, 1);
+ if (ProductionFlag.IS_INTERNAL) {
+ Stats.onSeparator(mLastComposedWord.mSeparatorString,
+ Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
}
- mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
- mHandler.cancelUpdateBigramPredictions();
- mHandler.postUpdateSuggestions();
- }
-
- // "ic" must not be null
- private boolean revertDoubleSpaceWhileInBatchEdit(final InputConnection ic) {
- mHandler.cancelDoubleSpacesTimer();
- // Here we test whether we indeed have a period and a space before us. This should not
- // be needed, but it's there just in case something went wrong.
- final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
- if (!". ".equals(textBeforeCursor)) {
- // Theoretically we should not be coming here if there isn't ". " before the
- // cursor, but the application may be changing the text while we are typing, so
- // anything goes. We should not crash.
- Log.d(TAG, "Tried to revert double-space combo but we didn't find "
- + "\". \" just before the cursor.");
- return false;
- }
- ic.deleteSurroundingText(2, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(2);
- }
- ic.commitText(" ", 1);
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_revertDoubleSpaceWhileInBatchEdit();
- }
- return true;
- }
-
- private static boolean revertSwapPunctuation(final InputConnection ic) {
- // Here we test whether we indeed have a space and something else before us. This should not
- // be needed, but it's there just in case something went wrong.
- final CharSequence textBeforeCursor = ic.getTextBeforeCursor(2, 0);
- // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
- // enter surrogate pairs this code will have been removed.
- if (TextUtils.isEmpty(textBeforeCursor)
- || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
- // We may only come here if the application is changing the text while we are typing.
- // This is quite a broken case, but not logically impossible, so we shouldn't crash,
- // but some debugging log may be in order.
- Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
- + "find a space just before the cursor.");
- return false;
+ ResearchLogger.latinIME_revertCommit(originallyTypedWord);
}
- ic.beginBatchEdit();
- ic.deleteSurroundingText(2, 0);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_deleteSurroundingText(2);
- }
- ic.commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
- if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.latinIME_revertSwapPunctuation();
- }
- ic.endBatchEdit();
- return true;
- }
-
- public boolean isWordSeparator(int code) {
- return mSettingsValues.isWordSeparator(code);
+ // Don't restart suggestion yet. We'll restart if the user deletes the
+ // separator.
+ mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+ // We have a separator between the word and the cursor: we should show predictions.
+ mHandler.postUpdateSuggestionStrip();
}
- public boolean preferCapitalization() {
- return mWordComposer.isFirstCharCapitalized();
+ // Used by the RingCharBuffer
+ public boolean isWordSeparator(final int code) {
+ return mCurrentSettings.isWordSeparator(code);
}
- // Notify that language or mode have been changed and toggleLanguage will update KeyboardID
- // according to new language or mode.
- public void onRefreshKeyboard() {
+ // TODO: Make this private
+ // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
+ /* package for test */
+ void loadKeyboard() {
// When the device locale is changed in SetupWizard etc., this method may get called via
// onConfigurationChanged before SoftInputWindow is shown.
- if (mKeyboardSwitcher.getKeyboardView() != null) {
- // Reload keyboard because the current language has been changed.
- mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mSettingsValues);
- }
initSuggest();
- updateCorrectionMode();
loadSettings();
+ if (mKeyboardSwitcher.getMainKeyboardView() != null) {
+ // Reload keyboard because the current language has been changed.
+ mKeyboardSwitcher.loadKeyboard(getCurrentInputEditorInfo(), mCurrentSettings);
+ }
// 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 updateBigramPredictions anyway).
- if (isCursorTouchingWord()) {
- mHandler.postUpdateSuggestions();
- } else {
- mHandler.postUpdateBigramPredictions();
- }
+ // predictions or punctuation signs (which is done by the updateSuggestionStrip anyway).
+ mHandler.postUpdateSuggestionStrip();
}
// TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to
- // {@link KeyboardSwitcher}.
+ // {@link KeyboardSwitcher}. Called from KeyboardSwitcher
public void hapticAndAudioFeedback(final int primaryCode) {
- mFeedbackManager.hapticAndAudioFeedback(primaryCode, mKeyboardSwitcher.getKeyboardView());
+ mFeedbackManager.hapticAndAudioFeedback(
+ primaryCode, mKeyboardSwitcher.getMainKeyboardView());
}
+ // Callback called by PointerTracker through the KeyboardActionListener. This is called when a
+ // key is depressed; release matching call is onReleaseKey below.
@Override
- public void onPressKey(int primaryCode) {
+ public void onPressKey(final int primaryCode) {
mKeyboardSwitcher.onPressKey(primaryCode);
}
+ // Callback by PointerTracker through the KeyboardActionListener. This is called when a key
+ // is released; press matching call is onPressKey above.
@Override
- public void onReleaseKey(int primaryCode, boolean withSliding) {
+ public void onReleaseKey(final int primaryCode, final boolean withSliding) {
mKeyboardSwitcher.onReleaseKey(primaryCode, withSliding);
// If accessibility is on, ensure the user receives keyboard state updates.
@@ -2352,12 +2304,10 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// 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.
- final InputConnection ic = getCurrentInputConnection();
- if (null != ic) {
- final CharSequence lastChar = ic.getTextBeforeCursor(1, 0);
- if (!TextUtils.isEmpty(lastChar) && Character.isHighSurrogate(lastChar.charAt(0))) {
- ic.deleteSurroundingText(1, 0);
- }
+ // 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);
}
}
}
@@ -2365,7 +2315,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
// receive ringer mode change and network state change.
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
- public void onReceive(Context context, Intent intent) {
+ public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
mSubtypeSwitcher.onNetworkStateChanged(intent);
@@ -2375,37 +2325,27 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
}
};
- private void updateCorrectionMode() {
- // TODO: cleanup messy flags
- final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
- && !mInputAttributes.mInputTypeNoAutoCorrect;
- mCorrectionMode = shouldAutoCorrect ? Suggest.CORRECTION_FULL : Suggest.CORRECTION_NONE;
- mCorrectionMode = (mSettingsValues.mBigramSuggestionEnabled && shouldAutoCorrect)
- ? Suggest.CORRECTION_FULL_BIGRAM : mCorrectionMode;
- }
-
- private void updateSuggestionVisibility(final Resources res) {
- final String suggestionVisiblityStr = mSettingsValues.mShowSuggestionsSetting;
- for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
- if (suggestionVisiblityStr.equals(res.getString(visibility))) {
- mSuggestionVisibility = visibility;
- break;
- }
- }
- }
-
private void launchSettings() {
- launchSettingsClass(SettingsActivity.class);
+ handleClose();
+ launchSubActivity(SettingsActivity.class);
}
+ // Called from debug code only
public void launchDebugSettings() {
- launchSettingsClass(DebugSettingsActivity.class);
+ handleClose();
+ launchSubActivity(DebugSettingsActivity.class);
}
- private void launchSettingsClass(Class<? extends PreferenceActivity> settingsClass) {
- handleClose();
+ public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) {
+ // Put the text in the attached EditText into a safe, saved state before switching to a
+ // new activity that will also use the soft keyboard.
+ commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+ launchSubActivity(activityClass);
+ }
+
+ private void launchSubActivity(final Class<? extends Activity> activityClass) {
Intent intent = new Intent();
- intent.setClass(LatinIME.this, settingsClass);
+ intent.setClass(LatinIME.this, activityClass);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
@@ -2440,12 +2380,14 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setItems(items, listener)
.setTitle(title);
- showOptionDialogInternal(builder.create());
+ showOptionDialog(builder.create());
}
- private void showOptionDialogInternal(AlertDialog dialog) {
- final IBinder windowToken = mKeyboardSwitcher.getKeyboardView().getWindowToken();
- if (windowToken == null) return;
+ public void showOptionDialog(final AlertDialog dialog) {
+ final IBinder windowToken = mKeyboardSwitcher.getMainKeyboardView().getWindowToken();
+ if (windowToken == null) {
+ return;
+ }
dialog.setCancelable(true);
dialog.setCanceledOnTouchOutside(true);
@@ -2461,8 +2403,19 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
dialog.show();
}
+ public void debugDumpStateAndCrashWithException(final String context) {
+ final StringBuilder s = new StringBuilder();
+ s.append("Target application : ").append(mTargetApplicationInfo.name)
+ .append("\nPackage : ").append(mTargetApplicationInfo.packageName)
+ .append("\nTarget app sdk version : ")
+ .append(mTargetApplicationInfo.targetSdkVersion)
+ .append("\nAttributes : ").append(mCurrentSettings.getInputAttributesDebugString())
+ .append("\nContext : ").append(context);
+ throw new RuntimeException(s.toString());
+ }
+
@Override
- protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
+ protected void dump(final FileDescriptor fd, final PrintWriter fout, final String[] args) {
super.dump(fd, fout, args);
final Printer p = new PrintWriterPrinter(fout);
@@ -2470,13 +2423,13 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
final int keyboardMode = keyboard != null ? keyboard.mId.mMode : -1;
p.println(" Keyboard mode = " + keyboardMode);
- p.println(" mIsSuggestionsRequested=" + mInputAttributes.mIsSettingsSuggestionStripOn);
- p.println(" mCorrectionMode=" + mCorrectionMode);
+ p.println(" mIsSuggestionsSuggestionsRequested = "
+ + mCurrentSettings.isSuggestionsRequested(mDisplayOrientation));
+ p.println(" mCorrectionEnabled=" + mCurrentSettings.mCorrectionEnabled);
p.println(" isComposingWord=" + mWordComposer.isComposingWord());
- p.println(" mAutoCorrectEnabled=" + mSettingsValues.mAutoCorrectEnabled);
- p.println(" mSoundOn=" + mSettingsValues.mSoundOn);
- p.println(" mVibrateOn=" + mSettingsValues.mVibrateOn);
- p.println(" mKeyPreviewPopupOn=" + mSettingsValues.mKeyPreviewPopupOn);
- p.println(" mInputAttributes=" + mInputAttributes.toString());
+ p.println(" mSoundOn=" + mCurrentSettings.mSoundOn);
+ p.println(" mVibrateOn=" + mCurrentSettings.mVibrateOn);
+ p.println(" mKeyPreviewPopupOn=" + mCurrentSettings.mKeyPreviewPopupOn);
+ p.println(" inputAttributes=" + mCurrentSettings.getInputAttributesDebugString());
}
}
diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
index dc0868e7c..394a9c7aa 100644
--- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java
+++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java
@@ -21,7 +21,7 @@ import android.view.inputmethod.EditorInfo;
import com.android.inputmethod.keyboard.Keyboard;
-public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
+public final class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
public static boolean sDBG = false;
public static boolean sVISUALDEBUG = false;
@@ -44,7 +44,12 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang
String before, String after, int position, SuggestedWords suggestedWords) {
}
- public static void logOnAutoCorrection(String before, String after, int separatorCode) {
+ public static void logOnAutoCorrectionForTyping(
+ String before, String after, int separatorCode) {
+ }
+
+ public static void logOnAutoCorrectionForGeometric(String before, String after,
+ int separatorCode, InputPointers inputPointers) {
}
public static void logOnAutoCorrectionCancelled() {
@@ -71,7 +76,7 @@ public class LatinImeLogger implements SharedPreferences.OnSharedPreferenceChang
public static void onStartSuggestion(CharSequence previousWords) {
}
- public static void onAddSuggestedWord(String word, int typeId, int dataType) {
+ public static void onAddSuggestedWord(String word, String sourceDictionaryId) {
}
public static void onSetKeyboard(Keyboard kb) {
diff --git a/java/src/com/android/inputmethod/latin/LocaleUtils.java b/java/src/com/android/inputmethod/latin/LocaleUtils.java
index b938dd336..feb1b2d0e 100644
--- a/java/src/com/android/inputmethod/latin/LocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/LocaleUtils.java
@@ -31,7 +31,10 @@ import java.util.Locale;
* update/bugfix to this file, consider also updating/fixing the version in the
* dictionary pack.
*/
-public class LocaleUtils {
+public final class LocaleUtils {
+ private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap();
+ private static final String LOCALE_AND_TIME_STR_SEPARATER = ",";
+
private LocaleUtils() {
// Intentional empty constructor for utility class.
}
@@ -193,7 +196,7 @@ public class LocaleUtils {
}
}
- private static final HashMap<String, Locale> sLocaleCache = new HashMap<String, Locale>();
+ private static final HashMap<String, Locale> sLocaleCache = CollectionUtils.newHashMap();
/**
* Creates a locale from a string specification.
@@ -219,4 +222,38 @@ public class LocaleUtils {
return retval;
}
}
+
+ public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) {
+ if (TextUtils.isEmpty(str)) {
+ return EMPTY_LT_HASH_MAP;
+ }
+ final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER);
+ final int N = ss.length;
+ if (N < 2 || N % 2 != 0) {
+ return EMPTY_LT_HASH_MAP;
+ }
+ final HashMap<String, Long> retval = CollectionUtils.newHashMap();
+ for (int i = 0; i < N / 2; ++i) {
+ final String localeStr = ss[i * 2];
+ final long time = Long.valueOf(ss[i * 2 + 1]);
+ retval.put(localeStr, time);
+ }
+ return retval;
+ }
+
+ public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) {
+ if (map == null || map.isEmpty()) {
+ return "";
+ }
+ final StringBuilder builder = new StringBuilder();
+ for (String localeStr : map.keySet()) {
+ if (builder.length() > 0) {
+ builder.append(LOCALE_AND_TIME_STR_SEPARATER);
+ }
+ final Long time = map.get(localeStr);
+ builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER);
+ builder.append(String.valueOf(time));
+ }
+ return builder.toString();
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java
deleted file mode 100644
index 66d6d58b1..000000000
--- a/java/src/com/android/inputmethod/latin/ResearchLogger.java
+++ /dev/null
@@ -1,757 +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.content.SharedPreferences;
-import android.inputmethodservice.InputMethodService;
-import android.os.Build;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Process;
-import android.os.SystemClock;
-import android.preference.PreferenceManager;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.inputmethod.CompletionInfo;
-import android.view.inputmethod.EditorInfo;
-
-import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.KeyDetector;
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.internal.KeyboardState;
-import com.android.inputmethod.latin.define.ProductionFlag;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.charset.Charset;
-import java.util.Map;
-
-/**
- * Logs the use of the LatinIME keyboard.
- *
- * This class logs operations on the IME keyboard, including what the user has typed.
- * Data is stored locally in a file in app-specific storage.
- *
- * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
- */
-public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
- private static final String TAG = ResearchLogger.class.getSimpleName();
- private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
- private static final boolean DEBUG = false;
-
- private static final ResearchLogger sInstance = new ResearchLogger(new LogFileManager());
- public static boolean sIsLogging = false;
- /* package */ final Handler mLoggingHandler;
- private InputMethodService mIms;
-
- /**
- * Isolates management of files. This variable should never be null, but can be changed
- * to support testing.
- */
- /* package */ LogFileManager mLogFileManager;
-
- /**
- * Manages the file(s) that stores the logs.
- *
- * Handles creation, deletion, and provides Readers, Writers, and InputStreams to access
- * the logs.
- */
- /* package */ static class LogFileManager {
- public static final String RESEARCH_LOG_FILENAME_KEY = "RESEARCH_LOG_FILENAME";
-
- private static final String DEFAULT_FILENAME = "researchLog.txt";
- private static final long LOGFILE_PURGE_INTERVAL = 1000 * 60 * 60 * 24;
-
- protected InputMethodService mIms;
- protected File mFile;
- protected PrintWriter mPrintWriter;
-
- /* package */ LogFileManager() {
- }
-
- public void init(final InputMethodService ims) {
- mIms = ims;
- }
-
- public synchronized void createLogFile() throws IOException {
- createLogFile(DEFAULT_FILENAME);
- }
-
- public synchronized void createLogFile(final SharedPreferences prefs)
- throws IOException {
- final String filename =
- prefs.getString(RESEARCH_LOG_FILENAME_KEY, DEFAULT_FILENAME);
- createLogFile(filename);
- }
-
- public synchronized void createLogFile(final String filename)
- throws IOException {
- if (mIms == null) {
- final String msg = "InputMethodService is not configured. Logging is off.";
- Log.w(TAG, msg);
- throw new IOException(msg);
- }
- final File filesDir = mIms.getFilesDir();
- if (filesDir == null || !filesDir.exists()) {
- final String msg = "Storage directory does not exist. Logging is off.";
- Log.w(TAG, msg);
- throw new IOException(msg);
- }
- close();
- final File file = new File(filesDir, filename);
- mFile = file;
- boolean append = true;
- if (file.exists() && file.lastModified() + LOGFILE_PURGE_INTERVAL <
- System.currentTimeMillis()) {
- append = false;
- }
- mPrintWriter = new PrintWriter(new BufferedWriter(new FileWriter(file, append)), true);
- }
-
- public synchronized boolean append(final String s) {
- PrintWriter printWriter = mPrintWriter;
- if (printWriter == null || !mFile.exists()) {
- if (DEBUG) {
- Log.w(TAG, "PrintWriter is null... attempting to create default log file");
- }
- try {
- createLogFile();
- printWriter = mPrintWriter;
- } catch (IOException e) {
- Log.w(TAG, "Failed to create log file. Not logging.");
- return false;
- }
- }
- printWriter.print(s);
- printWriter.flush();
- return !printWriter.checkError();
- }
-
- public synchronized void reset() {
- if (mPrintWriter != null) {
- mPrintWriter.close();
- mPrintWriter = null;
- if (DEBUG) {
- Log.d(TAG, "logfile closed");
- }
- }
- if (mFile != null) {
- mFile.delete();
- if (DEBUG) {
- Log.d(TAG, "logfile deleted");
- }
- mFile = null;
- }
- }
-
- public synchronized void close() {
- if (mPrintWriter != null) {
- mPrintWriter.close();
- mPrintWriter = null;
- mFile = null;
- if (DEBUG) {
- Log.d(TAG, "logfile closed");
- }
- }
- }
-
- /* package */ synchronized void flush() {
- if (mPrintWriter != null) {
- mPrintWriter.flush();
- }
- }
-
- /* package */ synchronized String getContents() {
- final File file = mFile;
- if (file == null) {
- return "";
- }
- if (mPrintWriter != null) {
- mPrintWriter.flush();
- }
- FileInputStream stream = null;
- FileChannel fileChannel = null;
- String s = "";
- try {
- stream = new FileInputStream(file);
- fileChannel = stream.getChannel();
- final ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
- fileChannel.read(byteBuffer);
- byteBuffer.rewind();
- CharBuffer charBuffer = Charset.defaultCharset().decode(byteBuffer);
- s = charBuffer.toString();
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if (fileChannel != null) {
- fileChannel.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if (stream != null) {
- stream.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- return s;
- }
- }
-
- private ResearchLogger(final LogFileManager logFileManager) {
- final HandlerThread handlerThread = new HandlerThread("ResearchLogger logging task",
- Process.THREAD_PRIORITY_BACKGROUND);
- handlerThread.start();
- mLoggingHandler = new Handler(handlerThread.getLooper());
- mLogFileManager = logFileManager;
- }
-
- public static ResearchLogger getInstance() {
- return sInstance;
- }
-
- public static void init(final InputMethodService ims, final SharedPreferences prefs) {
- sInstance.initInternal(ims, prefs);
- }
-
- /* package */ void initInternal(final InputMethodService ims, final SharedPreferences prefs) {
- mIms = ims;
- final LogFileManager logFileManager = mLogFileManager;
- if (logFileManager != null) {
- logFileManager.init(ims);
- try {
- logFileManager.createLogFile(prefs);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- if (prefs != null) {
- sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
- prefs.registerOnSharedPreferenceChangeListener(this);
- }
- }
-
- /**
- * Represents a category of logging events that share the same subfield structure.
- */
- private static enum LogGroup {
- MOTION_EVENT("m"),
- KEY("k"),
- CORRECTION("c"),
- STATE_CHANGE("s"),
- UNSTRUCTURED("u");
-
- private final String mLogString;
-
- private LogGroup(final String logString) {
- mLogString = logString;
- }
- }
-
- public void logMotionEvent(final int action, final long eventTime, final int id,
- final int x, final int y, final float size, final float pressure) {
- final String eventTag;
- switch (action) {
- case MotionEvent.ACTION_CANCEL: eventTag = "[Cancel]"; break;
- 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;
- case MotionEvent.ACTION_OUTSIDE: eventTag = "[Outside]"; break;
- default: eventTag = "[Action" + action + "]"; break;
- }
- if (!TextUtils.isEmpty(eventTag)) {
- final StringBuilder sb = new StringBuilder();
- sb.append(eventTag);
- sb.append('\t'); sb.append(eventTime);
- sb.append('\t'); sb.append(id);
- sb.append('\t'); sb.append(x);
- sb.append('\t'); sb.append(y);
- sb.append('\t'); sb.append(size);
- sb.append('\t'); sb.append(pressure);
- write(LogGroup.MOTION_EVENT, sb.toString());
- }
- }
-
- public void logKeyEvent(final int code, final int x, final int y) {
- final StringBuilder sb = new StringBuilder();
- sb.append(Keyboard.printableCode(code));
- sb.append('\t'); sb.append(x);
- sb.append('\t'); sb.append(y);
- write(LogGroup.KEY, sb.toString());
- }
-
- public void logCorrection(final String subgroup, final String before, final String after,
- final int position) {
- final StringBuilder sb = new StringBuilder();
- sb.append(subgroup);
- sb.append('\t'); sb.append(before);
- sb.append('\t'); sb.append(after);
- sb.append('\t'); sb.append(position);
- write(LogGroup.CORRECTION, sb.toString());
- }
-
- public void logStateChange(final String subgroup, final String details) {
- write(LogGroup.STATE_CHANGE, subgroup + "\t" + details);
- }
-
- public static class UnsLogGroup {
- private static final boolean DEFAULT_ENABLED = true;
-
- private static final boolean KEYBOARDSTATE_ONCANCELINPUT_ENABLED = DEFAULT_ENABLED;
- private static final boolean KEYBOARDSTATE_ONCODEINPUT_ENABLED = DEFAULT_ENABLED;
- private static final boolean KEYBOARDSTATE_ONLONGPRESSTIMEOUT_ENABLED = DEFAULT_ENABLED;
- private static final boolean KEYBOARDSTATE_ONPRESSKEY_ENABLED = DEFAULT_ENABLED;
- private static final boolean KEYBOARDSTATE_ONRELEASEKEY_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_COMMITCURRENTAUTOCORRECTION_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_COMMITTEXT_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_DELETESURROUNDINGTEXT_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_DOUBLESPACEAUTOPERIOD_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_ONDISPLAYCOMPLETIONS_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_ONSTARTINPUTVIEWINTERNAL_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_ONUPDATESELECTION_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_PERFORMEDITORACTION_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean LATINIME_PICKPUNCTUATIONSUGGESTION_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_PICKSUGGESTIONMANUALLY_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_REVERTCOMMIT_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean LATINIME_REVERTSWAPPUNCTUATION_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_SENDKEYCODEPOINT_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean LATINIME_SWITCHTOKEYBOARDVIEW_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINKEYBOARDVIEW_ONLONGPRESS_ENABLED = DEFAULT_ENABLED;
- private static final boolean LATINKEYBOARDVIEW_ONPROCESSMOTIONEVENT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean LATINKEYBOARDVIEW_SETKEYBOARD_ENABLED = DEFAULT_ENABLED;
- private static final boolean POINTERTRACKER_CALLLISTENERONCANCELINPUT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean POINTERTRACKER_CALLLISTENERONCODEINPUT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean
- POINTERTRACKER_CALLLISTENERONPRESSANDCHECKKEYBOARDLAYOUTCHANGE_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean POINTERTRACKER_CALLLISTENERONRELEASE_ENABLED = DEFAULT_ENABLED;
- private static final boolean POINTERTRACKER_ONDOWNEVENT_ENABLED = DEFAULT_ENABLED;
- private static final boolean POINTERTRACKER_ONMOVEEVENT_ENABLED = DEFAULT_ENABLED;
- private static final boolean SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT_ENABLED
- = DEFAULT_ENABLED;
- private static final boolean SUGGESTIONSVIEW_SETSUGGESTIONS_ENABLED = DEFAULT_ENABLED;
- }
-
- public static void logUnstructured(String logGroup, final String details) {
- // TODO: improve performance by making entire class static and/or implementing natively
- getInstance().write(LogGroup.UNSTRUCTURED, logGroup + "\t" + details);
- }
-
- private void write(final LogGroup logGroup, final String log) {
- // TODO: rewrite in native for better performance
- mLoggingHandler.post(new Runnable() {
- @Override
- public void run() {
- final long currentTime = System.currentTimeMillis();
- final long upTime = SystemClock.uptimeMillis();
- final StringBuilder builder = new StringBuilder();
- builder.append(currentTime);
- builder.append('\t'); builder.append(upTime);
- builder.append('\t'); builder.append(logGroup.mLogString);
- builder.append('\t'); builder.append(log);
- builder.append('\n');
- if (DEBUG) {
- Log.d(TAG, "Write: " + '[' + logGroup.mLogString + ']' + log);
- }
- final String s = builder.toString();
- if (mLogFileManager.append(s)) {
- // success
- } else {
- if (DEBUG) {
- Log.w(TAG, "Unable to write to log.");
- }
- // perhaps logfile was deleted. try to recreate and relog.
- try {
- mLogFileManager.createLogFile(PreferenceManager
- .getDefaultSharedPreferences(mIms));
- mLogFileManager.append(s);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- });
- }
-
- public void clearAll() {
- mLoggingHandler.post(new Runnable() {
- @Override
- public void run() {
- if (DEBUG) {
- Log.d(TAG, "Delete log file.");
- }
- mLogFileManager.reset();
- }
- });
- }
-
- /* package */ LogFileManager getLogFileManager() {
- return mLogFileManager;
- }
-
- @Override
- public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
- if (key == null || prefs == null) {
- return;
- }
- sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
- }
-
- public static void keyboardState_onCancelInput(final boolean isSinglePointer,
- final KeyboardState keyboardState) {
- if (UnsLogGroup.KEYBOARDSTATE_ONCANCELINPUT_ENABLED) {
- final String s = "onCancelInput: single=" + isSinglePointer + " " + keyboardState;
- logUnstructured("KeyboardState_onCancelInput", s);
- }
- }
-
- public static void keyboardState_onCodeInput(
- final int code, final boolean isSinglePointer, final int autoCaps,
- final KeyboardState keyboardState) {
- if (UnsLogGroup.KEYBOARDSTATE_ONCODEINPUT_ENABLED) {
- final String s = "onCodeInput: code=" + Keyboard.printableCode(code)
- + " single=" + isSinglePointer
- + " autoCaps=" + autoCaps + " " + keyboardState;
- logUnstructured("KeyboardState_onCodeInput", s);
- }
- }
-
- public static void keyboardState_onLongPressTimeout(final int code,
- final KeyboardState keyboardState) {
- if (UnsLogGroup.KEYBOARDSTATE_ONLONGPRESSTIMEOUT_ENABLED) {
- final String s = "onLongPressTimeout: code=" + Keyboard.printableCode(code) + " "
- + keyboardState;
- logUnstructured("KeyboardState_onLongPressTimeout", s);
- }
- }
-
- public static void keyboardState_onPressKey(final int code,
- final KeyboardState keyboardState) {
- if (UnsLogGroup.KEYBOARDSTATE_ONPRESSKEY_ENABLED) {
- final String s = "onPressKey: code=" + Keyboard.printableCode(code) + " "
- + keyboardState;
- logUnstructured("KeyboardState_onPressKey", s);
- }
- }
-
- public static void keyboardState_onReleaseKey(final KeyboardState keyboardState, final int code,
- final boolean withSliding) {
- if (UnsLogGroup.KEYBOARDSTATE_ONRELEASEKEY_ENABLED) {
- final String s = "onReleaseKey: code=" + Keyboard.printableCode(code)
- + " sliding=" + withSliding + " " + keyboardState;
- logUnstructured("KeyboardState_onReleaseKey", s);
- }
- }
-
- public static void latinIME_commitCurrentAutoCorrection(final String typedWord,
- final String autoCorrection) {
- if (UnsLogGroup.LATINIME_COMMITCURRENTAUTOCORRECTION_ENABLED) {
- if (typedWord.equals(autoCorrection)) {
- getInstance().logCorrection("[----]", typedWord, autoCorrection, -1);
- } else {
- getInstance().logCorrection("[Auto]", typedWord, autoCorrection, -1);
- }
- }
- }
-
- public static void latinIME_commitText(final CharSequence typedWord) {
- if (UnsLogGroup.LATINIME_COMMITTEXT_ENABLED) {
- logUnstructured("LatinIME_commitText", typedWord.toString());
- }
- }
-
- public static void latinIME_deleteSurroundingText(final int length) {
- if (UnsLogGroup.LATINIME_DELETESURROUNDINGTEXT_ENABLED) {
- logUnstructured("LatinIME_deleteSurroundingText", String.valueOf(length));
- }
- }
-
- public static void latinIME_doubleSpaceAutoPeriod() {
- if (UnsLogGroup.LATINIME_DOUBLESPACEAUTOPERIOD_ENABLED) {
- logUnstructured("LatinIME_doubleSpaceAutoPeriod", "");
- }
- }
-
- public static void latinIME_onDisplayCompletions(
- final CompletionInfo[] applicationSpecifiedCompletions) {
- if (UnsLogGroup.LATINIME_ONDISPLAYCOMPLETIONS_ENABLED) {
- final StringBuilder builder = new StringBuilder();
- builder.append("Received completions:");
- if (applicationSpecifiedCompletions != null) {
- for (int i = 0; i < applicationSpecifiedCompletions.length; i++) {
- builder.append(" #");
- builder.append(i);
- builder.append(": ");
- builder.append(applicationSpecifiedCompletions[i]);
- builder.append("\n");
- }
- }
- logUnstructured("LatinIME_onDisplayCompletions", builder.toString());
- }
- }
-
- public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
- final SharedPreferences prefs) {
- if (UnsLogGroup.LATINIME_ONSTARTINPUTVIEWINTERNAL_ENABLED) {
- final StringBuilder builder = new StringBuilder();
- builder.append("onStartInputView: editorInfo:");
- builder.append("\tinputType=");
- builder.append(Integer.toHexString(editorInfo.inputType));
- builder.append("\timeOptions=");
- builder.append(Integer.toHexString(editorInfo.imeOptions));
- builder.append("\tdisplay="); builder.append(Build.DISPLAY);
- builder.append("\tmodel="); builder.append(Build.MODEL);
- for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
- builder.append("\t" + entry.getKey());
- Object value = entry.getValue();
- builder.append("=" + ((value == null) ? "<null>" : value.toString()));
- }
- logUnstructured("LatinIME_onStartInputViewInternal", builder.toString());
- }
- }
-
- public static void latinIME_onUpdateSelection(final int lastSelectionStart,
- final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
- final int newSelStart, final int newSelEnd, final int composingSpanStart,
- final int composingSpanEnd) {
- if (UnsLogGroup.LATINIME_ONUPDATESELECTION_ENABLED) {
- final String s = "onUpdateSelection: oss=" + oldSelStart
- + ", ose=" + oldSelEnd
- + ", lss=" + lastSelectionStart
- + ", lse=" + lastSelectionEnd
- + ", nss=" + newSelStart
- + ", nse=" + newSelEnd
- + ", cs=" + composingSpanStart
- + ", ce=" + composingSpanEnd;
- logUnstructured("LatinIME_onUpdateSelection", s);
- }
- }
-
- public static void latinIME_performEditorAction(final int imeActionNext) {
- if (UnsLogGroup.LATINIME_PERFORMEDITORACTION_ENABLED) {
- logUnstructured("LatinIME_performEditorAction", String.valueOf(imeActionNext));
- }
- }
-
- public static void latinIME_pickApplicationSpecifiedCompletion(final int index,
- final CharSequence text, int x, int y) {
- if (UnsLogGroup.LATINIME_PICKAPPLICATIONSPECIFIEDCOMPLETION_ENABLED) {
- final String s = String.valueOf(index) + '\t' + text + '\t' + x + '\t' + y;
- logUnstructured("LatinIME_pickApplicationSpecifiedCompletion", s);
- }
- }
-
- public static void latinIME_pickSuggestionManually(final String replacedWord,
- final int index, CharSequence suggestion, int x, int y) {
- if (UnsLogGroup.LATINIME_PICKSUGGESTIONMANUALLY_ENABLED) {
- final String s = String.valueOf(index) + '\t' + suggestion + '\t' + x + '\t' + y;
- logUnstructured("LatinIME_pickSuggestionManually", s);
- }
- }
-
- public static void latinIME_punctuationSuggestion(final int index,
- final CharSequence suggestion, int x, int y) {
- if (UnsLogGroup.LATINIME_PICKPUNCTUATIONSUGGESTION_ENABLED) {
- final String s = String.valueOf(index) + '\t' + suggestion + '\t' + x + '\t' + y;
- logUnstructured("LatinIME_pickPunctuationSuggestion", s);
- }
- }
-
- public static void latinIME_revertDoubleSpaceWhileInBatchEdit() {
- if (UnsLogGroup.LATINIME_REVERTDOUBLESPACEWHILEINBATCHEDIT_ENABLED) {
- logUnstructured("LatinIME_revertDoubleSpaceWhileInBatchEdit", "");
- }
- }
-
- public static void latinIME_revertSwapPunctuation() {
- if (UnsLogGroup.LATINIME_REVERTSWAPPUNCTUATION_ENABLED) {
- logUnstructured("LatinIME_revertSwapPunctuation", "");
- }
- }
-
- public static void latinIME_sendKeyCodePoint(final int code) {
- if (UnsLogGroup.LATINIME_SENDKEYCODEPOINT_ENABLED) {
- logUnstructured("LatinIME_sendKeyCodePoint", String.valueOf(code));
- }
- }
-
- public static void latinIME_swapSwapperAndSpaceWhileInBatchEdit() {
- if (UnsLogGroup.LATINIME_SWAPSWAPPERANDSPACEWHILEINBATCHEDIT_ENABLED) {
- logUnstructured("latinIME_swapSwapperAndSpaceWhileInBatchEdit", "");
- }
- }
-
- public static void latinIME_switchToKeyboardView() {
- if (UnsLogGroup.LATINIME_SWITCHTOKEYBOARDVIEW_ENABLED) {
- final String s = "Switch to keyboard view.";
- logUnstructured("LatinIME_switchToKeyboardView", s);
- }
- }
-
- public static void latinKeyboardView_onLongPress() {
- if (UnsLogGroup.LATINKEYBOARDVIEW_ONLONGPRESS_ENABLED) {
- final String s = "long press detected";
- logUnstructured("LatinKeyboardView_onLongPress", s);
- }
- }
-
- public static void latinKeyboardView_processMotionEvent(MotionEvent me, int action,
- long eventTime, int index, int id, int x, int y) {
- if (UnsLogGroup.LATINKEYBOARDVIEW_ONPROCESSMOTIONEVENT_ENABLED) {
- final float size = me.getSize(index);
- final float pressure = me.getPressure(index);
- if (action != MotionEvent.ACTION_MOVE) {
- getInstance().logMotionEvent(action, eventTime, id, x, y, size, pressure);
- }
- }
- }
-
- public static void latinKeyboardView_setKeyboard(final Keyboard keyboard) {
- if (UnsLogGroup.LATINKEYBOARDVIEW_SETKEYBOARD_ENABLED) {
- StringBuilder builder = new StringBuilder();
- builder.append("id=");
- builder.append(keyboard.mId);
- builder.append("\tw=");
- builder.append(keyboard.mOccupiedWidth);
- builder.append("\th=");
- builder.append(keyboard.mOccupiedHeight);
- builder.append("\tkeys=[");
- boolean first = true;
- for (Key key : keyboard.mKeys) {
- if (first) {
- first = false;
- } else {
- builder.append(",");
- }
- builder.append("{code:");
- builder.append(key.mCode);
- builder.append(",altCode:");
- builder.append(key.mAltCode);
- builder.append(",x:");
- builder.append(key.mX);
- builder.append(",y:");
- builder.append(key.mY);
- builder.append(",w:");
- builder.append(key.mWidth);
- builder.append(",h:");
- builder.append(key.mHeight);
- builder.append("}");
- }
- builder.append("]");
- logUnstructured("LatinKeyboardView_setKeyboard", builder.toString());
- }
- }
-
- public static void latinIME_revertCommit(final String originallyTypedWord) {
- if (UnsLogGroup.LATINIME_REVERTCOMMIT_ENABLED) {
- logUnstructured("LatinIME_revertCommit", originallyTypedWord);
- }
- }
-
- public static void pointerTracker_callListenerOnCancelInput() {
- final String s = "onCancelInput";
- if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONCANCELINPUT_ENABLED) {
- logUnstructured("PointerTracker_callListenerOnCancelInput", s);
- }
- }
-
- public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
- final int y, final boolean ignoreModifierKey, final boolean altersCode,
- final int code) {
- if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONCODEINPUT_ENABLED) {
- final String s = "onCodeInput: " + Keyboard.printableCode(code)
- + " text=" + key.mOutputText + " x=" + x + " y=" + y
- + " ignoreModifier=" + ignoreModifierKey + " altersCode=" + altersCode
- + " enabled=" + key.isEnabled();
- logUnstructured("PointerTracker_callListenerOnCodeInput", s);
- }
- }
-
- public static void pointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange(
- final Key key, final boolean ignoreModifierKey) {
- if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONPRESSANDCHECKKEYBOARDLAYOUTCHANGE_ENABLED) {
- final String s = "onPress : " + KeyDetector.printableCode(key)
- + " ignoreModifier=" + ignoreModifierKey
- + " enabled=" + key.isEnabled();
- logUnstructured("PointerTracker_callListenerOnPressAndCheckKeyboardLayoutChange", s);
- }
- }
-
- public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
- final boolean withSliding, final boolean ignoreModifierKey) {
- if (UnsLogGroup.POINTERTRACKER_CALLLISTENERONRELEASE_ENABLED) {
- final String s = "onRelease : " + Keyboard.printableCode(primaryCode)
- + " sliding=" + withSliding + " ignoreModifier=" + ignoreModifierKey
- + " enabled="+ key.isEnabled();
- logUnstructured("PointerTracker_callListenerOnRelease", s);
- }
- }
-
- public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
- if (UnsLogGroup.POINTERTRACKER_ONDOWNEVENT_ENABLED) {
- final String s = "onDownEvent: ignore potential noise: time=" + deltaT
- + " distance=" + distanceSquared;
- logUnstructured("PointerTracker_onDownEvent", s);
- }
- }
-
- public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
- final int lastY) {
- if (UnsLogGroup.POINTERTRACKER_ONMOVEEVENT_ENABLED) {
- final String s = String.format("onMoveEvent: sudden move is translated to "
- + "up[%d,%d]/down[%d,%d] events", lastX, lastY, x, y);
- logUnstructured("PointerTracker_onMoveEvent", s);
- }
- }
-
- public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
- if (UnsLogGroup.SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT_ENABLED) {
- final String s = "onTouchEvent: ignore sudden jump " + me;
- logUnstructured("SuddenJumpingTouchEventHandler_onTouchEvent", s);
- }
- }
-
- public static void suggestionsView_setSuggestions(final SuggestedWords mSuggestedWords) {
- if (UnsLogGroup.SUGGESTIONSVIEW_SETSUGGESTIONS_ENABLED) {
- logUnstructured("SuggestionsView_setSuggestions", mSuggestedWords.toString());
- }
- }
-} \ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/ResizableIntArray.java b/java/src/com/android/inputmethod/latin/ResizableIntArray.java
new file mode 100644
index 000000000..9a46f160b
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ResizableIntArray.java
@@ -0,0 +1,146 @@
+/*
+ * 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 java.util.Arrays;
+
+// TODO: This class is not thread-safe.
+public final class ResizableIntArray {
+ private int[] mArray;
+ private int mLength;
+
+ public ResizableIntArray(final int capacity) {
+ reset(capacity);
+ }
+
+ public int get(final int index) {
+ if (index < mLength) {
+ return mArray[index];
+ }
+ throw new ArrayIndexOutOfBoundsException("length=" + mLength + "; index=" + index);
+ }
+
+ public void add(final int index, final int val) {
+ if (index < mLength) {
+ mArray[index] = val;
+ } else {
+ mLength = index;
+ add(val);
+ }
+ }
+
+ public void add(final int val) {
+ final int currentLength = mLength;
+ ensureCapacity(currentLength + 1);
+ mArray[currentLength] = val;
+ mLength = currentLength + 1;
+ }
+
+ /**
+ * Calculate the new capacity of {@code mArray}.
+ * @param minimumCapacity the minimum capacity that the {@code mArray} should have.
+ * @return the new capacity that the {@code mArray} should have. Returns zero when there is no
+ * need to expand {@code mArray}.
+ */
+ private int calculateCapacity(final int minimumCapacity) {
+ final int currentCapcity = mArray.length;
+ if (currentCapcity < minimumCapacity) {
+ final int nextCapacity = currentCapcity * 2;
+ // The following is the same as return Math.max(minimumCapacity, nextCapacity);
+ return minimumCapacity > nextCapacity ? minimumCapacity : nextCapacity;
+ }
+ return 0;
+ }
+
+ private void ensureCapacity(final int minimumCapacity) {
+ final int newCapacity = calculateCapacity(minimumCapacity);
+ if (newCapacity > 0) {
+ // TODO: Implement primitive array pool.
+ mArray = Arrays.copyOf(mArray, newCapacity);
+ }
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+
+ public void setLength(final int newLength) {
+ ensureCapacity(newLength);
+ mLength = newLength;
+ }
+
+ public void reset(final int capacity) {
+ // TODO: Implement primitive array pool.
+ mArray = new int[capacity];
+ mLength = 0;
+ }
+
+ public int[] getPrimitiveArray() {
+ return mArray;
+ }
+
+ public void set(final ResizableIntArray ip) {
+ // TODO: Implement primitive array pool.
+ mArray = ip.mArray;
+ mLength = ip.mLength;
+ }
+
+ public void copy(final ResizableIntArray ip) {
+ final int newCapacity = calculateCapacity(ip.mLength);
+ if (newCapacity > 0) {
+ // TODO: Implement primitive array pool.
+ mArray = new int[newCapacity];
+ }
+ System.arraycopy(ip.mArray, 0, mArray, 0, ip.mLength);
+ mLength = ip.mLength;
+ }
+
+ public void append(final ResizableIntArray src, final int startPos, final int length) {
+ if (length == 0) {
+ return;
+ }
+ final int currentLength = mLength;
+ final int newLength = currentLength + length;
+ ensureCapacity(newLength);
+ System.arraycopy(src.mArray, startPos, mArray, currentLength, length);
+ mLength = newLength;
+ }
+
+ public void fill(final int value, final int startPos, final int length) {
+ if (startPos < 0 || length < 0) {
+ throw new IllegalArgumentException("startPos=" + startPos + "; length=" + length);
+ }
+ final int endPos = startPos + length;
+ ensureCapacity(endPos);
+ Arrays.fill(mArray, startPos, endPos, value);
+ if (mLength < endPos) {
+ mLength = endPos;
+ }
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < mLength; i++) {
+ if (i != 0) {
+ sb.append(",");
+ }
+ sb.append(mArray[i]);
+ }
+ return "[" + sb + "]";
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/ResourceUtils.java b/java/src/com/android/inputmethod/latin/ResourceUtils.java
new file mode 100644
index 000000000..5021ad384
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ResourceUtils.java
@@ -0,0 +1,128 @@
+/*
+ * 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.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.util.TypedValue;
+
+import java.util.HashMap;
+
+public final class ResourceUtils {
+ public static final float UNDEFINED_RATIO = -1.0f;
+ public static final int UNDEFINED_DIMENSION = -1;
+
+ private ResourceUtils() {
+ // This utility class is not publicly instantiable.
+ }
+
+ private static final String HARDWARE_PREFIX = Build.HARDWARE + ",";
+ private static final HashMap<String, String> sDeviceOverrideValueMap =
+ CollectionUtils.newHashMap();
+
+ public static String getDeviceOverrideValue(Resources res, int overrideResId, String defValue) {
+ final int orientation = res.getConfiguration().orientation;
+ final String key = overrideResId + "-" + orientation;
+ if (!sDeviceOverrideValueMap.containsKey(key)) {
+ String overrideValue = defValue;
+ for (final String element : res.getStringArray(overrideResId)) {
+ if (element.startsWith(HARDWARE_PREFIX)) {
+ overrideValue = element.substring(HARDWARE_PREFIX.length());
+ break;
+ }
+ }
+ sDeviceOverrideValueMap.put(key, overrideValue);
+ }
+ return sDeviceOverrideValueMap.get(key);
+ }
+
+ public static boolean isValidFraction(final float fraction) {
+ return fraction >= 0.0f;
+ }
+
+ // {@link Resources#getDimensionPixelSize(int)} returns at least one pixel size.
+ public static boolean isValidDimensionPixelSize(final int dimension) {
+ return dimension > 0;
+ }
+
+ // {@link Resources#getDimensionPixelOffset(int)} may return zero pixel offset.
+ public static boolean isValidDimensionPixelOffset(final int dimension) {
+ return dimension >= 0;
+ }
+
+ public static float getFraction(final TypedArray a, final int index, final float defValue) {
+ final TypedValue value = a.peekValue(index);
+ if (value == null || !isFractionValue(value)) {
+ return defValue;
+ }
+ return a.getFraction(index, 1, 1, defValue);
+ }
+
+ public static float getFraction(final TypedArray a, final int index) {
+ return getFraction(a, index, UNDEFINED_RATIO);
+ }
+
+ public static int getDimensionPixelSize(final TypedArray a, final int index) {
+ final TypedValue value = a.peekValue(index);
+ if (value == null || !isDimensionValue(value)) {
+ return ResourceUtils.UNDEFINED_DIMENSION;
+ }
+ return a.getDimensionPixelSize(index, ResourceUtils.UNDEFINED_DIMENSION);
+ }
+
+ public static float getDimensionOrFraction(TypedArray a, int index, int base,
+ float defValue) {
+ final TypedValue value = a.peekValue(index);
+ if (value == null) {
+ return defValue;
+ }
+ if (isFractionValue(value)) {
+ return a.getFraction(index, base, base, defValue);
+ } else if (isDimensionValue(value)) {
+ return a.getDimension(index, defValue);
+ }
+ return defValue;
+ }
+
+ public static int getEnumValue(TypedArray a, int index, int defValue) {
+ final TypedValue value = a.peekValue(index);
+ if (value == null) {
+ return defValue;
+ }
+ if (isIntegerValue(value)) {
+ return a.getInt(index, defValue);
+ }
+ return defValue;
+ }
+
+ public static boolean isFractionValue(TypedValue v) {
+ return v.type == TypedValue.TYPE_FRACTION;
+ }
+
+ public static boolean isDimensionValue(TypedValue v) {
+ return v.type == TypedValue.TYPE_DIMENSION;
+ }
+
+ public static boolean isIntegerValue(TypedValue v) {
+ return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT;
+ }
+
+ public static boolean isStringValue(TypedValue v) {
+ return v.type == TypedValue.TYPE_STRING;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
new file mode 100644
index 000000000..21441369e
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -0,0 +1,709 @@
+/*
+ * 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.inputmethodservice.InputMethodService;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
+
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+/**
+ * Enrichment class for InputConnection to simplify interaction and add functionality.
+ *
+ * This class serves as a wrapper to be able to simply add hooks to any calls to the underlying
+ * InputConnection. It also keeps track of a number of things to avoid having to call upon IPC
+ * all the time to find out what text is in the buffer, when we need it to determine caps mode
+ * for example.
+ */
+public final class RichInputConnection {
+ private static final String TAG = RichInputConnection.class.getSimpleName();
+ private static final boolean DBG = false;
+ 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 = BinaryDictionary.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.
+ */
+ private int mCurrentCursorPosition = 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.
+ */
+ private StringBuilder mCommittedTextBeforeComposingText = new StringBuilder();
+ /**
+ * This contains the currently composing text, as LatinIME thinks the TextView is seeing it.
+ */
+ private StringBuilder mComposingText = new StringBuilder();
+ /**
+ * This is a one-character string containing the character after the cursor. Since LatinIME
+ * never touches it directly, it's never modified by any means other than re-reading from the
+ * TextView when the cursor position is changed by the user.
+ */
+ private CharSequence mCharAfterTheCursor = "";
+ // 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;
+ int mNestLevel;
+ public RichInputConnection(final InputMethodService parent) {
+ mParent = parent;
+ mIC = null;
+ mNestLevel = 0;
+ }
+
+ private void checkConsistencyForDebug() {
+ final ExtractedTextRequest r = new ExtractedTextRequest();
+ r.hintMaxChars = 0;
+ r.hintMaxLines = 0;
+ r.token = 1;
+ r.flags = 0;
+ final ExtractedText et = mIC.getExtractedText(r, 0);
+ final CharSequence beforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
+ final StringBuilder internal = new StringBuilder().append(mCommittedTextBeforeComposingText)
+ .append(mComposingText);
+ if (null == et || null == beforeCursor) return;
+ final int actualLength = Math.min(beforeCursor.length(), internal.length());
+ if (internal.length() > actualLength) {
+ internal.delete(0, internal.length() - actualLength);
+ }
+ final String reference = (beforeCursor.length() <= actualLength) ? beforeCursor.toString()
+ : beforeCursor.subSequence(beforeCursor.length() - actualLength,
+ beforeCursor.length()).toString();
+ if (et.selectionStart != mCurrentCursorPosition
+ || !(reference.equals(internal.toString()))) {
+ final String context = "Expected cursor position = " + mCurrentCursorPosition
+ + "\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);
+ }
+ }
+
+ public void beginBatchEdit() {
+ if (++mNestLevel == 1) {
+ mIC = mParent.getCurrentInputConnection();
+ if (null != mIC) {
+ mIC.beginBatchEdit();
+ }
+ } else {
+ if (DBG) {
+ throw new RuntimeException("Nest level too deep");
+ } else {
+ Log.e(TAG, "Nest level too deep : " + mNestLevel);
+ }
+ }
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ }
+
+ public void endBatchEdit() {
+ if (mNestLevel <= 0) Log.e(TAG, "Batch edit not in progress!"); // TODO: exception instead
+ if (--mNestLevel == 0 && null != mIC) {
+ mIC.endBatchEdit();
+ }
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ }
+
+ public void resetCachesUponCursorMove(final int newCursorPosition) {
+ mCurrentCursorPosition = newCursorPosition;
+ mComposingText.setLength(0);
+ mCommittedTextBeforeComposingText.setLength(0);
+ mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
+ mCharAfterTheCursor = getTextAfterCursor(1, 0);
+ if (null != mIC) {
+ mIC.finishComposingText();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_finishComposingText();
+ }
+ }
+ }
+
+ private void checkBatchEdit() {
+ if (mNestLevel != 1) {
+ // TODO: exception instead
+ Log.e(TAG, "Batch edit level incorrect : " + mNestLevel);
+ Log.e(TAG, Utils.getStackTrace(4));
+ }
+ }
+
+ public void finishComposingText() {
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ mCommittedTextBeforeComposingText.append(mComposingText);
+ mCurrentCursorPosition += mComposingText.length();
+ mComposingText.setLength(0);
+ if (null != mIC) {
+ mIC.finishComposingText();
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_finishComposingText();
+ }
+ }
+ }
+
+ public void commitText(final CharSequence text, final int i) {
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ mCommittedTextBeforeComposingText.append(text);
+ mCurrentCursorPosition += text.length() - mComposingText.length();
+ mComposingText.setLength(0);
+ if (null != mIC) {
+ mIC.commitText(text, i);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_commitText(text, i);
+ }
+ }
+ }
+
+ /**
+ * Gets the caps modes we should be in after this specific string.
+ *
+ * This returns a bit set of TextUtils#CAP_MODE_*, masked by the inputType argument.
+ * This method also supports faking an additional space after the string passed in argument,
+ * to support cases where a space will be added automatically, like in phantom space
+ * state for example.
+ * Note that for English, we are using American typography rules (which are not specific to
+ * American English, it's just the most common set of rules for English).
+ *
+ * @param inputType a mask of the caps modes to test for.
+ * @param locale what language should be considered.
+ * @param hasSpaceBefore if we should consider there should be a space after the string.
+ * @return the caps modes that should be on as a set of bits
+ */
+ public int getCursorCapsMode(final int inputType, final Locale locale,
+ final boolean hasSpaceBefore) {
+ mIC = mParent.getCurrentInputConnection();
+ if (null == mIC) return Constants.TextUtils.CAP_MODE_OFF;
+ if (!TextUtils.isEmpty(mComposingText)) {
+ if (hasSpaceBefore) {
+ // If we have some composing text and a space before, then we should have
+ // MODE_CHARACTERS and MODE_WORDS on.
+ return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & inputType;
+ } else {
+ // We have some composing text - we should be in MODE_CHARACTERS only.
+ return TextUtils.CAP_MODE_CHARACTERS & inputType;
+ }
+ }
+ // TODO: this will generally work, but there may be cases where the buffer contains SOME
+ // information but not enough to determine the caps mode accurately. This may happen after
+ // 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));
+ }
+ // This never calls InputConnection#getCapsMode - in fact, it's a static method that
+ // never blocks or initiates IPC.
+ return StringUtils.getCapsMode(mCommittedTextBeforeComposingText, inputType, locale,
+ hasSpaceBefore);
+ }
+
+ public int getCodePointBeforeCursor() {
+ if (mCommittedTextBeforeComposingText.length() < 1) return Constants.NOT_A_CODE;
+ return Character.codePointBefore(mCommittedTextBeforeComposingText,
+ mCommittedTextBeforeComposingText.length());
+ }
+
+ public CharSequence getTextBeforeCursor(final int i, final int j) {
+ // TODO: use mCommittedTextBeforeComposingText if possible to improve performance
+ mIC = mParent.getCurrentInputConnection();
+ if (null != mIC) return mIC.getTextBeforeCursor(i, j);
+ return null;
+ }
+
+ public CharSequence getTextAfterCursor(final int i, final int j) {
+ mIC = mParent.getCurrentInputConnection();
+ if (null != mIC) return mIC.getTextAfterCursor(i, j);
+ return null;
+ }
+
+ public void deleteSurroundingText(final int i, final int j) {
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ final int remainingChars = mComposingText.length() - i;
+ if (remainingChars >= 0) {
+ mComposingText.setLength(remainingChars);
+ } else {
+ mComposingText.setLength(0);
+ // Never cut under 0
+ final int len = Math.max(mCommittedTextBeforeComposingText.length()
+ + remainingChars, 0);
+ mCommittedTextBeforeComposingText.setLength(len);
+ }
+ if (mCurrentCursorPosition > i) {
+ mCurrentCursorPosition -= i;
+ } else {
+ mCurrentCursorPosition = 0;
+ }
+ if (null != mIC) {
+ mIC.deleteSurroundingText(i, j);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_deleteSurroundingText(i, j);
+ }
+ }
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ }
+
+ public void performEditorAction(final int actionId) {
+ mIC = mParent.getCurrentInputConnection();
+ if (null != mIC) {
+ mIC.performEditorAction(actionId);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_performEditorAction(actionId);
+ }
+ }
+ }
+
+ public void sendKeyEvent(final KeyEvent keyEvent) {
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ // This method is only called for enter or backspace when speaking to old
+ // applications (target SDK <= 15), or for digits.
+ // When talking to new applications we never use this method because it's inherently
+ // racy and has unpredictable results, but for backward compatibility we continue
+ // sending the key events for only Enter and Backspace because some applications
+ // mistakenly catch them to do some stuff.
+ switch (keyEvent.getKeyCode()) {
+ case KeyEvent.KEYCODE_ENTER:
+ mCommittedTextBeforeComposingText.append("\n");
+ mCurrentCursorPosition += 1;
+ break;
+ case KeyEvent.KEYCODE_DEL:
+ if (0 == mComposingText.length()) {
+ if (mCommittedTextBeforeComposingText.length() > 0) {
+ mCommittedTextBeforeComposingText.delete(
+ mCommittedTextBeforeComposingText.length() - 1,
+ mCommittedTextBeforeComposingText.length());
+ }
+ } else {
+ mComposingText.delete(mComposingText.length() - 1, mComposingText.length());
+ }
+ if (mCurrentCursorPosition > 0) mCurrentCursorPosition -= 1;
+ break;
+ case KeyEvent.KEYCODE_UNKNOWN:
+ if (null != keyEvent.getCharacters()) {
+ mCommittedTextBeforeComposingText.append(keyEvent.getCharacters());
+ mCurrentCursorPosition += keyEvent.getCharacters().length();
+ }
+ break;
+ default:
+ final String text = new String(new int[] { keyEvent.getUnicodeChar() }, 0, 1);
+ mCommittedTextBeforeComposingText.append(text);
+ mCurrentCursorPosition += text.length();
+ break;
+ }
+ }
+ if (null != mIC) {
+ mIC.sendKeyEvent(keyEvent);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_sendKeyEvent(keyEvent);
+ }
+ }
+ }
+
+ public void setComposingText(final CharSequence text, final int i) {
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ mCurrentCursorPosition += 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);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_setComposingText(text, i);
+ }
+ }
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ }
+
+ public void setSelection(final int from, final int to) {
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ if (null != mIC) {
+ mIC.setSelection(from, to);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_setSelection(from, to);
+ }
+ }
+ mCurrentCursorPosition = from;
+ mCommittedTextBeforeComposingText.setLength(0);
+ mCommittedTextBeforeComposingText.append(getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0));
+ }
+
+ public void commitCorrection(final CorrectionInfo correctionInfo) {
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ // This has no effect on the text field and does not change its content. It only makes
+ // TextView flash the text for a second based on indices contained in the argument.
+ if (null != mIC) {
+ mIC.commitCorrection(correctionInfo);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_commitCorrection(correctionInfo);
+ }
+ }
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ }
+
+ public void commitCompletion(final CompletionInfo completionInfo) {
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ final CharSequence text = completionInfo.getText();
+ mCommittedTextBeforeComposingText.append(text);
+ mCurrentCursorPosition += text.length() - mComposingText.length();
+ mComposingText.setLength(0);
+ if (null != mIC) {
+ mIC.commitCompletion(completionInfo);
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ ResearchLogger.richInputConnection_commitCompletion(completionInfo);
+ }
+ }
+ if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
+ }
+
+ public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) {
+ mIC = mParent.getCurrentInputConnection();
+ if (null == mIC) return null;
+ final CharSequence prev = mIC.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()
+ : prev.subSequence(prev.length() - checkLength, prev.length()).toString();
+ final StringBuilder internal = new StringBuilder()
+ .append(mCommittedTextBeforeComposingText).append(mComposingText);
+ if (internal.length() > checkLength) {
+ internal.delete(0, internal.length() - checkLength);
+ if (!(reference.equals(internal.toString()))) {
+ final String context =
+ "Expected text = " + internal + "\nActual text = " + reference;
+ ((LatinIME)mParent).debugDumpStateAndCrashWithException(context);
+ }
+ }
+ }
+ 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 String mWord;
+
+ public Range(int charsBefore, int charsAfter, String 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;
+ }
+
+ // Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor,
+ // n = 2 retrieves the word before that, and so on. This splits on whitespace only.
+ // Also, it won't return words that end in a separator (if the nth word before the cursor
+ // ends in a separator, it returns null).
+ // Example :
+ // (n = 1) "abc def|" -> def
+ // (n = 1) "abc def |" -> def
+ // (n = 1) "abc def. |" -> null
+ // (n = 1) "abc def . |" -> null
+ // (n = 2) "abc def|" -> abc
+ // (n = 2) "abc def |" -> abc
+ // (n = 2) "abc def. |" -> abc
+ // (n = 2) "abc def . |" -> def
+ // (n = 2) "abc|" -> null
+ // (n = 2) "abc |" -> null
+ // (n = 2) "abc. def|" -> null
+ public static CharSequence getNthPreviousWord(final CharSequence prev,
+ final String sentenceSeperators, final int n) {
+ if (prev == null) return null;
+ String[] w = spaceRegex.split(prev);
+
+ // If we can't find n words, or we found an empty word, return null.
+ if (w.length < n || w[w.length - n].length() <= 0) return null;
+
+ // If ends in a separator, return null
+ char lastChar = w[w.length - n].charAt(w[w.length - n].length() - 1);
+ if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
+
+ return w[w.length - n];
+ }
+
+ /**
+ * @param separators characters which may separate words
+ * @return the word that surrounds the cursor, including up to one trailing
+ * separator. For example, if the field contains "he|llo world", where |
+ * represents the cursor, then "hello " will be returned.
+ */
+ public String getWordAtCursor(String separators) {
+ // getWordRangeAtCursor returns null if the connection is null
+ Range r = getWordRangeAtCursor(separators, 0);
+ return (r == null) ? null : r.mWord;
+ }
+
+ private int getCursorPosition() {
+ mIC = mParent.getCurrentInputConnection();
+ if (null == mIC) return INVALID_CURSOR_POSITION;
+ final ExtractedText extracted = mIC.getExtractedText(new ExtractedTextRequest(), 0);
+ if (extracted == null) {
+ return INVALID_CURSOR_POSITION;
+ }
+ return extracted.startOffset + extracted.selectionStart;
+ }
+
+ /**
+ * Returns the text surrounding the cursor.
+ *
+ * @param sep a string of characters that split words.
+ * @param additionalPrecedingWordsCount the number of words before the current word that should
+ * be included in the returned range
+ * @return a range containing the text surrounding the cursor
+ */
+ public Range getWordRangeAtCursor(String sep, int additionalPrecedingWordsCount) {
+ mIC = mParent.getCurrentInputConnection();
+ if (mIC == null || sep == null) {
+ return null;
+ }
+ CharSequence before = mIC.getTextBeforeCursor(1000, 0);
+ CharSequence after = mIC.getTextAfterCursor(1000, 0);
+ if (before == null || after == null) {
+ return null;
+ }
+
+ // Going backward, alternate skipping non-separators and separators until enough words
+ // have been read.
+ int start = before.length();
+ boolean isStoppingAtWhitespace = true; // toggles to indicate what to stop at
+ while (true) { // see comments below for why this is guaranteed to halt
+ while (start > 0) {
+ final int codePoint = Character.codePointBefore(before, start);
+ if (isStoppingAtWhitespace == isSeparator(codePoint, sep)) {
+ break; // inner loop
+ }
+ --start;
+ if (Character.isSupplementaryCodePoint(codePoint)) {
+ --start;
+ }
+ }
+ // isStoppingAtWhitespace is true every other time through the loop,
+ // so additionalPrecedingWordsCount is guaranteed to become < 0, which
+ // guarantees outer loop termination
+ if (isStoppingAtWhitespace && (--additionalPrecedingWordsCount < 0)) {
+ break; // outer loop
+ }
+ isStoppingAtWhitespace = !isStoppingAtWhitespace;
+ }
+
+ // Find last word separator after the cursor
+ int end = -1;
+ while (++end < after.length()) {
+ final int codePoint = Character.codePointAt(after, end);
+ if (isSeparator(codePoint, sep)) {
+ break;
+ }
+ if (Character.isSupplementaryCodePoint(codePoint)) {
+ ++end;
+ }
+ }
+
+ int cursor = getCursorPosition();
+ if (start >= 0 && cursor + end <= after.length() + before.length()) {
+ String word = before.toString().substring(start, before.length())
+ + after.toString().substring(0, end);
+ return new Range(before.length() - start, end, word);
+ }
+
+ return null;
+ }
+
+ public boolean isCursorTouchingWord(final SettingsValues settingsValues) {
+ CharSequence before = getTextBeforeCursor(1, 0);
+ CharSequence after = getTextAfterCursor(1, 0);
+ if (!TextUtils.isEmpty(before) && !settingsValues.isWordSeparator(before.charAt(0))
+ && !settingsValues.isSymbolExcludedFromWordSeparators(before.charAt(0))) {
+ return true;
+ }
+ if (!TextUtils.isEmpty(after) && !settingsValues.isWordSeparator(after.charAt(0))
+ && !settingsValues.isSymbolExcludedFromWordSeparators(after.charAt(0))) {
+ return true;
+ }
+ return false;
+ }
+
+ public void removeTrailingSpace() {
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ final CharSequence lastOne = getTextBeforeCursor(1, 0);
+ if (lastOne != null && lastOne.length() == 1
+ && lastOne.charAt(0) == Keyboard.CODE_SPACE) {
+ deleteSurroundingText(1, 0);
+ }
+ }
+
+ public boolean sameAsTextBeforeCursor(final CharSequence text) {
+ final CharSequence beforeText = getTextBeforeCursor(text.length(), 0);
+ return TextUtils.equals(text, beforeText);
+ }
+
+ /* (non-javadoc)
+ * Returns the word before the cursor if the cursor is at the end of a word, null otherwise
+ */
+ public CharSequence getWordBeforeCursorIfAtEndOfWord(final SettingsValues settings) {
+ // Bail out if the cursor is in the middle of a word (cursor must be followed by whitespace,
+ // separator or end of line/text)
+ // Example: "test|"<EOL> "te|st" get rejected here
+ final CharSequence textAfterCursor = getTextAfterCursor(1, 0);
+ if (!TextUtils.isEmpty(textAfterCursor)
+ && !settings.isWordSeparator(textAfterCursor.charAt(0))) return null;
+
+ // Bail out if word before cursor is 0-length or a single non letter (like an apostrophe)
+ // Example: " -|" gets rejected here but "e-|" and "e|" are okay
+ CharSequence word = getWordAtCursor(settings.mWordSeparators);
+ // We don't suggest on leading single quotes, so we have to remove them from the word if
+ // it starts with single quotes.
+ while (!TextUtils.isEmpty(word) && Keyboard.CODE_SINGLE_QUOTE == word.charAt(0)) {
+ word = word.subSequence(1, word.length());
+ }
+ if (TextUtils.isEmpty(word)) return null;
+ // Find the last code point of the string
+ final int lastCodePoint = Character.codePointBefore(word, word.length());
+ // If for some reason the text field contains non-unicode binary data, or if the
+ // charsequence is exactly one char long and the contents is a low surrogate, return null.
+ if (!Character.isDefined(lastCodePoint)) return null;
+ // Bail out if the cursor is not at the end of a word (cursor must be preceded by
+ // non-whitespace, non-separator, non-start-of-text)
+ // Example ("|" is the cursor here) : <SOL>"|a" " |a" " | " all get rejected here.
+ if (settings.isWordSeparator(lastCodePoint)) return null;
+ final char firstChar = word.charAt(0); // we just tested that word is not empty
+ if (word.length() == 1 && !Character.isLetter(firstChar)) return null;
+
+ // We only suggest on words that start with a letter or a symbol that is excluded from
+ // word separators (see #handleCharacterWhileInBatchEdit).
+ if (!(Character.isLetter(firstChar)
+ || settings.isSymbolExcludedFromWordSeparators(firstChar))) {
+ return null;
+ }
+
+ return word;
+ }
+
+ public boolean revertDoubleSpace() {
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ // Here we test whether we indeed have a period and a space before us. This should not
+ // be needed, but it's there just in case something went wrong.
+ final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
+ if (!". ".equals(textBeforeCursor)) {
+ // Theoretically we should not be coming here if there isn't ". " before the
+ // cursor, but the application may be changing the text while we are typing, so
+ // anything goes. We should not crash.
+ Log.d(TAG, "Tried to revert double-space combo but we didn't find "
+ + "\". \" just before the cursor.");
+ return false;
+ }
+ deleteSurroundingText(2, 0);
+ commitText(" ", 1);
+ return true;
+ }
+
+ public boolean revertSwapPunctuation() {
+ if (DEBUG_BATCH_NESTING) checkBatchEdit();
+ // Here we test whether we indeed have a space and something else before us. This should not
+ // be needed, but it's there just in case something went wrong.
+ final CharSequence textBeforeCursor = getTextBeforeCursor(2, 0);
+ // NOTE: This does not work with surrogate pairs. Hopefully when the keyboard is able to
+ // enter surrogate pairs this code will have been removed.
+ if (TextUtils.isEmpty(textBeforeCursor)
+ || (Keyboard.CODE_SPACE != textBeforeCursor.charAt(1))) {
+ // We may only come here if the application is changing the text while we are typing.
+ // This is quite a broken case, but not logically impossible, so we shouldn't crash,
+ // but some debugging log may be in order.
+ Log.d(TAG, "Tried to revert a swap of punctuation but we didn't "
+ + "find a space just before the cursor.");
+ return false;
+ }
+ deleteSurroundingText(2, 0);
+ commitText(" " + textBeforeCursor.subSequence(0, 1), 1);
+ return true;
+ }
+
+ /**
+ * Heuristic to determine if this is an expected update of the cursor.
+ *
+ * Sometimes updates to the cursor position are late because of their asynchronous nature.
+ * This method tries to determine if this update is one, based on the values of the cursor
+ * position in the update, and the currently expected position of the cursor according to
+ * LatinIME's internal accounting. If this is not a belated expected update, then it should
+ * mean that the user moved the cursor explicitly.
+ * This is quite robust, but of course it's not perfect. In particular, it will fail in the
+ * case we get an update A, the user types in N characters so as to move the cursor to A+N but
+ * we don't get those, and then the user places the cursor between A and A+N, and we get only
+ * this update and not the ones in-between. This is almost impossible to achieve even trying
+ * very very hard.
+ *
+ * @param oldSelStart The value of the old cursor position in the update.
+ * @param newSelStart The value of the new cursor position in the update.
+ * @return whether this is a belated expected update or not.
+ */
+ public boolean isBelatedExpectedUpdate(final int oldSelStart, final int newSelStart) {
+ // If this is an update that arrives at our expected position, it's a belated update.
+ if (newSelStart == mCurrentCursorPosition) 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;
+ // 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;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 4bb21720b..238724610 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -41,9 +41,8 @@ import android.widget.TextView;
import com.android.inputmethod.latin.define.ProductionFlag;
import com.android.inputmethodcommon.InputMethodSettingsFragment;
-public class Settings extends InputMethodSettingsFragment
+public final class Settings extends InputMethodSettingsFragment
implements SharedPreferences.OnSharedPreferenceChangeListener {
- public static final boolean ENABLE_EXPERIMENTAL_SETTINGS = false;
// In the same order as xml/prefs.xml
public static final String PREF_GENERAL_SETTINGS = "general_settings";
@@ -57,25 +56,26 @@ public class Settings extends InputMethodSettingsFragment
public static final String PREF_AUTO_CORRECTION_THRESHOLD = "auto_correction_threshold";
public static final String PREF_SHOW_SUGGESTIONS_SETTING = "show_suggestions_setting";
public static final String PREF_MISC_SETTINGS = "misc_settings";
- public static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
public static final String PREF_LAST_USER_DICTIONARY_WRITE_TIME =
"last_user_dictionary_write_time";
public static final String PREF_ADVANCED_SETTINGS = "pref_advanced_settings";
- public static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
- "pref_suppress_language_switch_key";
+ public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
+ public static final String PREF_SHOW_LANGUAGE_SWITCH_KEY =
+ "pref_show_language_switch_key";
public static final String PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST =
"pref_include_other_imes_in_language_switch_list";
public static final String PREF_CUSTOM_INPUT_STYLES = "custom_input_styles";
public static final String PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY =
"pref_key_preview_popup_dismiss_delay";
- public static final String PREF_KEY_USE_CONTACTS_DICT = "pref_key_use_contacts_dict";
- public static final String PREF_BIGRAM_SUGGESTION = "next_word_suggestion";
public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
- public static final String PREF_KEY_ENABLE_SPAN_INSERT = "enable_span_insert";
+ public static final String PREF_GESTURE_INPUT = "gesture_input";
public static final String PREF_VIBRATION_DURATION_SETTINGS =
"pref_vibration_duration_settings";
public static final String PREF_KEYPRESS_SOUND_VOLUME =
"pref_keypress_sound_volume";
+ public static final String PREF_GESTURE_PREVIEW_TRAIL = "pref_gesture_preview_trail";
+ public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT =
+ "pref_gesture_floating_preview_text";
public static final String PREF_INPUT_LANGUAGE = "input_language";
public static final String PREF_SELECTED_LANGUAGES = "selected_languages";
@@ -87,27 +87,28 @@ public class Settings extends InputMethodSettingsFragment
private ListPreference mShowCorrectionSuggestionsPreference;
private ListPreference mAutoCorrectionThresholdPreference;
private ListPreference mKeyPreviewPopupDismissDelay;
- // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
- private CheckBoxPreference mBigramSuggestion;
- // Prediction: use bigrams to predict the next word when there is no input for it yet
+ // Use bigrams to predict the next word when there is no input for it yet
private CheckBoxPreference mBigramPrediction;
private Preference mDebugSettingsPreference;
private TextView mKeypressVibrationDurationSettingsTextView;
private TextView mKeypressSoundVolumeSettingsTextView;
+ private static void setPreferenceEnabled(final Preference preference, final boolean enabled) {
+ if (preference != null) {
+ preference.setEnabled(enabled);
+ }
+ }
+
private void ensureConsistencyOfAutoCorrectionSettings() {
final String autoCorrectionOff = getResources().getString(
R.string.auto_correction_threshold_mode_index_off);
final String currentSetting = mAutoCorrectionThresholdPreference.getValue();
- mBigramSuggestion.setEnabled(!currentSetting.equals(autoCorrectionOff));
- if (null != mBigramPrediction) {
- mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff));
- }
+ setPreferenceEnabled(mBigramPrediction, !currentSetting.equals(autoCorrectionOff));
}
@Override
- public void onCreate(Bundle icicle) {
+ public void onCreate(final Bundle icicle) {
super.onCreate(icicle);
setInputMethodSettingsCategoryTitle(R.string.language_selection_title);
setSubtypeEnablerTitle(R.string.select_language);
@@ -128,16 +129,7 @@ public class Settings extends InputMethodSettingsFragment
mAutoCorrectionThresholdPreference =
(ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD);
- mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTION);
mBigramPrediction = (CheckBoxPreference) findPreference(PREF_BIGRAM_PREDICTIONS);
- mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS);
- if (mDebugSettingsPreference != null) {
- final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN);
- debugSettingsIntent.setClassName(
- context.getPackageName(), DebugSettings.class.getName());
- mDebugSettingsPreference.setIntent(debugSettingsIntent);
- }
-
ensureConsistencyOfAutoCorrectionSettings();
final PreferenceGroup generalSettings =
@@ -147,6 +139,18 @@ public class Settings extends InputMethodSettingsFragment
final PreferenceGroup miscSettings =
(PreferenceGroup) findPreference(PREF_MISC_SETTINGS);
+ mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS);
+ if (mDebugSettingsPreference != null) {
+ if (ProductionFlag.IS_INTERNAL) {
+ final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN);
+ debugSettingsIntent.setClassName(
+ context.getPackageName(), DebugSettingsActivity.class.getName());
+ mDebugSettingsPreference.setIntent(debugSettingsIntent);
+ } else {
+ miscSettings.removePreference(mDebugSettingsPreference);
+ }
+ }
+
final boolean showVoiceKeyOption = res.getBoolean(
R.bool.config_enable_show_voice_key_option);
if (!showVoiceKeyOption) {
@@ -155,9 +159,6 @@ public class Settings extends InputMethodSettingsFragment
final PreferenceGroup advancedSettings =
(PreferenceGroup) findPreference(PREF_ADVANCED_SETTINGS);
- // Remove those meaningless options for now. TODO: delete them for good
- advancedSettings.removePreference(findPreference(PREF_BIGRAM_SUGGESTION));
- advancedSettings.removePreference(findPreference(PREF_KEY_ENABLE_SPAN_INSERT));
if (!VibratorUtils.getInstance(context).hasVibrator()) {
generalSettings.removePreference(findPreference(PREF_VIBRATE_ON));
if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
@@ -165,42 +166,34 @@ public class Settings extends InputMethodSettingsFragment
}
}
- final boolean showPopupOption = res.getBoolean(
+ final boolean showKeyPreviewPopupOption = res.getBoolean(
R.bool.config_enable_show_popup_on_keypress_option);
- if (!showPopupOption) {
+ mKeyPreviewPopupDismissDelay =
+ (ListPreference) findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
+ if (!showKeyPreviewPopupOption) {
generalSettings.removePreference(findPreference(PREF_POPUP_ON));
- }
-
- final boolean showBigramSuggestionsOption = res.getBoolean(
- R.bool.config_enable_next_word_suggestions_option);
- if (!showBigramSuggestionsOption) {
- textCorrectionGroup.removePreference(mBigramSuggestion);
- if (null != mBigramPrediction) {
- textCorrectionGroup.removePreference(mBigramPrediction);
+ if (null != advancedSettings) { // Theoretically advancedSettings cannot be null
+ advancedSettings.removePreference(mKeyPreviewPopupDismissDelay);
+ }
+ } else {
+ final String[] entries = new String[] {
+ res.getString(R.string.key_preview_popup_dismiss_no_delay),
+ res.getString(R.string.key_preview_popup_dismiss_default_delay),
+ };
+ final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
+ R.integer.config_key_preview_linger_timeout));
+ mKeyPreviewPopupDismissDelay.setEntries(entries);
+ mKeyPreviewPopupDismissDelay.setEntryValues(
+ new String[] { "0", popupDismissDelayDefaultValue });
+ if (null == mKeyPreviewPopupDismissDelay.getValue()) {
+ mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
}
+ setPreferenceEnabled(mKeyPreviewPopupDismissDelay,
+ SettingsValues.isKeyPreviewPopupEnabled(prefs, res));
}
- final CheckBoxPreference includeOtherImesInLanguageSwitchList =
- (CheckBoxPreference)findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST);
- includeOtherImesInLanguageSwitchList.setEnabled(
- !SettingsValues.isLanguageSwitchKeySupressed(prefs));
-
- mKeyPreviewPopupDismissDelay =
- (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
- final String[] entries = new String[] {
- res.getString(R.string.key_preview_popup_dismiss_no_delay),
- res.getString(R.string.key_preview_popup_dismiss_default_delay),
- };
- final String popupDismissDelayDefaultValue = Integer.toString(res.getInteger(
- R.integer.config_key_preview_linger_timeout));
- mKeyPreviewPopupDismissDelay.setEntries(entries);
- mKeyPreviewPopupDismissDelay.setEntryValues(
- new String[] { "0", popupDismissDelayDefaultValue });
- if (null == mKeyPreviewPopupDismissDelay.getValue()) {
- mKeyPreviewPopupDismissDelay.setValue(popupDismissDelayDefaultValue);
- }
- mKeyPreviewPopupDismissDelay.setEnabled(
- SettingsValues.isKeyPreviewPopupEnabled(prefs, res));
+ setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST),
+ SettingsValues.showsLanguageSwitchKey(prefs));
final PreferenceScreen dictionaryLink =
(PreferenceScreen) findPreference(PREF_CONFIGURE_DICTIONARIES_KEY);
@@ -211,21 +204,19 @@ public class Settings extends InputMethodSettingsFragment
textCorrectionGroup.removePreference(dictionaryLink);
}
- final boolean showUsabilityStudyModeOption =
- res.getBoolean(R.bool.config_enable_usability_study_mode_option)
- || ProductionFlag.IS_EXPERIMENTAL || ENABLE_EXPERIMENTAL_SETTINGS;
- final Preference usabilityStudyPref = findPreference(PREF_USABILITY_STUDY_MODE);
- if (!showUsabilityStudyModeOption) {
- if (usabilityStudyPref != null) {
- miscSettings.removePreference(usabilityStudyPref);
- }
- }
- if (ProductionFlag.IS_EXPERIMENTAL) {
- if (usabilityStudyPref instanceof CheckBoxPreference) {
- CheckBoxPreference checkbox = (CheckBoxPreference)usabilityStudyPref;
- checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE, true));
- checkbox.setSummary(R.string.settings_warning_researcher_mode);
- }
+ final boolean gestureInputEnabledByBuildConfig = res.getBoolean(
+ R.bool.config_gesture_input_enabled_by_build_config);
+ final Preference gesturePreviewTrail = findPreference(PREF_GESTURE_PREVIEW_TRAIL);
+ final Preference gestureFloatingPreviewText = findPreference(
+ PREF_GESTURE_FLOATING_PREVIEW_TEXT);
+ if (!gestureInputEnabledByBuildConfig) {
+ miscSettings.removePreference(findPreference(PREF_GESTURE_INPUT));
+ miscSettings.removePreference(gesturePreviewTrail);
+ miscSettings.removePreference(gestureFloatingPreviewText);
+ } else {
+ final boolean gestureInputEnabledByUser = prefs.getBoolean(PREF_GESTURE_INPUT, true);
+ setPreferenceEnabled(gesturePreviewTrail, gestureInputEnabledByUser);
+ setPreferenceEnabled(gestureFloatingPreviewText, gestureInputEnabledByUser);
}
mKeypressVibrationDurationSettingsPref =
@@ -280,20 +271,25 @@ public class Settings extends InputMethodSettingsFragment
}
@Override
- public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
(new BackupManager(getActivity())).dataChanged();
if (key.equals(PREF_POPUP_ON)) {
- final ListPreference popupDismissDelay =
- (ListPreference)findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY);
- if (null != popupDismissDelay) {
- popupDismissDelay.setEnabled(prefs.getBoolean(PREF_POPUP_ON, true));
+ setPreferenceEnabled(findPreference(PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY),
+ prefs.getBoolean(PREF_POPUP_ON, true));
+ } else if (key.equals(PREF_SHOW_LANGUAGE_SWITCH_KEY)) {
+ setPreferenceEnabled(findPreference(PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST),
+ SettingsValues.showsLanguageSwitchKey(prefs));
+ } else if (key.equals(PREF_GESTURE_INPUT)) {
+ final boolean gestureInputEnabledByConfig = getResources().getBoolean(
+ R.bool.config_gesture_input_enabled_by_build_config);
+ if (gestureInputEnabledByConfig) {
+ final boolean gestureInputEnabledByUser = prefs.getBoolean(
+ PREF_GESTURE_INPUT, true);
+ setPreferenceEnabled(findPreference(PREF_GESTURE_PREVIEW_TRAIL),
+ gestureInputEnabledByUser);
+ setPreferenceEnabled(findPreference(PREF_GESTURE_FLOATING_PREVIEW_TEXT),
+ gestureInputEnabledByUser);
}
- } else if (key.equals(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY)) {
- final CheckBoxPreference includeOtherImesInLanguageSwicthList =
- (CheckBoxPreference)findPreference(
- PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST);
- includeOtherImesInLanguageSwicthList.setEnabled(
- !SettingsValues.isLanguageSwitchKeySupressed(prefs));
}
ensureConsistencyOfAutoCorrectionSettings();
updateVoiceModeSummary();
@@ -327,33 +323,37 @@ public class Settings extends InputMethodSettingsFragment
private void updateKeyPreviewPopupDelaySummary() {
final ListPreference lp = mKeyPreviewPopupDismissDelay;
- lp.setSummary(lp.getEntries()[lp.findIndexOfValue(lp.getValue())]);
+ final CharSequence[] entries = lp.getEntries();
+ if (entries == null || entries.length <= 0) return;
+ lp.setSummary(entries[lp.findIndexOfValue(lp.getValue())]);
}
private void updateVoiceModeSummary() {
mVoicePreference.setSummary(
getResources().getStringArray(R.array.voice_input_modes_summary)
- [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
+ [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
}
private void refreshEnablingsOfKeypressSoundAndVibrationSettings(
- SharedPreferences sp, Resources res) {
+ final SharedPreferences sp, final Resources res) {
if (mKeypressVibrationDurationSettingsPref != null) {
- final boolean hasVibrator = VibratorUtils.getInstance(getActivity()).hasVibrator();
- final boolean vibrateOn = hasVibrator && sp.getBoolean(Settings.PREF_VIBRATE_ON,
+ final boolean hasVibratorHardware = VibratorUtils.getInstance(getActivity())
+ .hasVibrator();
+ final boolean vibrateOnByUser = sp.getBoolean(Settings.PREF_VIBRATE_ON,
res.getBoolean(R.bool.config_default_vibration_enabled));
- mKeypressVibrationDurationSettingsPref.setEnabled(vibrateOn);
+ setPreferenceEnabled(mKeypressVibrationDurationSettingsPref,
+ hasVibratorHardware && vibrateOnByUser);
}
if (mKeypressSoundVolumeSettingsPref != null) {
final boolean soundOn = sp.getBoolean(Settings.PREF_SOUND_ON,
res.getBoolean(R.bool.config_default_sound_enabled));
- mKeypressSoundVolumeSettingsPref.setEnabled(soundOn);
+ setPreferenceEnabled(mKeypressSoundVolumeSettingsPref, soundOn);
}
}
private void updateKeypressVibrationDurationSettingsSummary(
- SharedPreferences sp, Resources res) {
+ final SharedPreferences sp, final Resources res) {
if (mKeypressVibrationDurationSettingsPref != null) {
mKeypressVibrationDurationSettingsPref.setSummary(
SettingsValues.getCurrentVibrationDuration(sp, res)
@@ -411,7 +411,7 @@ public class Settings extends InputMethodSettingsFragment
builder.create().show();
}
- private void updateKeypressSoundVolumeSummary(SharedPreferences sp, Resources res) {
+ private void updateKeypressSoundVolumeSummary(final SharedPreferences sp, final Resources res) {
if (mKeypressSoundVolumeSettingsPref != null) {
mKeypressSoundVolumeSettingsPref.setSummary(String.valueOf(
(int)(SettingsValues.getCurrentKeypressSoundVolume(sp, res) * 100)));
diff --git a/java/src/com/android/inputmethod/latin/SettingsActivity.java b/java/src/com/android/inputmethod/latin/SettingsActivity.java
index 68f8582fc..0d3c8ebb7 100644
--- a/java/src/com/android/inputmethod/latin/SettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/SettingsActivity.java
@@ -19,7 +19,7 @@ package com.android.inputmethod.latin;
import android.content.Intent;
import android.preference.PreferenceActivity;
-public class SettingsActivity extends PreferenceActivity {
+public final class SettingsActivity extends PreferenceActivity {
private static final String DEFAULT_FRAGMENT = Settings.class.getName();
@Override
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index b07c3e59f..2a778aa0d 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -18,6 +18,7 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.content.SharedPreferences;
+import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
@@ -29,15 +30,27 @@ import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
-import java.util.Map;
/**
* When you call the constructor of this class, you may want to change the current system locale by
* using {@link LocaleUtils.RunInLocale}.
*/
-public class SettingsValues {
+public final class SettingsValues {
private static final String TAG = SettingsValues.class.getSimpleName();
+ private static final int SUGGESTION_VISIBILITY_SHOW_VALUE
+ = R.string.prefs_suggestion_visibility_show_value;
+ private static final int SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
+ = R.string.prefs_suggestion_visibility_show_only_portrait_value;
+ private static final int SUGGESTION_VISIBILITY_HIDE_VALUE
+ = R.string.prefs_suggestion_visibility_hide_value;
+
+ private static final int[] SUGGESTION_VISIBILITY_VALUE_ARRAY = new int[] {
+ SUGGESTION_VISIBILITY_SHOW_VALUE,
+ SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE,
+ SUGGESTION_VISIBILITY_HIDE_VALUE
+ };
+
// From resources:
public final int mDelayUpdateOldSuggestions;
public final String mWeakSpaceStrippers;
@@ -59,31 +72,37 @@ public class SettingsValues {
@SuppressWarnings("unused") // TODO: Use this
private final boolean mUsabilityStudyMode;
public final boolean mIncludesOtherImesInLanguageSwitchList;
- public final boolean mIsLanguageSwitchKeySuppressed;
+ public final boolean mShowsLanguageSwitchKey;
@SuppressWarnings("unused") // TODO: Use this
private final String mKeyPreviewPopupDismissDelayRawValue;
public final boolean mUseContactsDict;
- // Suggestion: use bigrams to adjust scores of suggestions obtained from unigram dictionary
- public final boolean mBigramSuggestionEnabled;
- // Prediction: use bigrams to predict the next word when there is no input for it yet
+ // Use bigrams to predict the next word when there is no input for it yet
public final boolean mBigramPredictionEnabled;
- public final boolean mEnableSuggestionSpanInsertion;
@SuppressWarnings("unused") // TODO: Use this
private final int mVibrationDurationSettingsRawValue;
@SuppressWarnings("unused") // TODO: Use this
private final float mKeypressSoundVolumeRawValue;
private final InputMethodSubtype[] mAdditionalSubtypes;
+ public final boolean mGestureInputEnabled;
+ public final boolean mGesturePreviewTrailEnabled;
+ public final boolean mGestureFloatingPreviewTextEnabled;
+
+ // From the input box
+ private final InputAttributes mInputAttributes;
// Deduced settings
public final int mKeypressVibrationDuration;
public final float mFxVolume;
public final int mKeyPreviewPopupDismissDelay;
- public final boolean mAutoCorrectEnabled;
+ private final boolean mAutoCorrectEnabled;
public final float mAutoCorrectionThreshold;
+ public final boolean mCorrectionEnabled;
+ public final int mSuggestionVisibility;
private final boolean mVoiceKeyEnabled;
private final boolean mVoiceKeyOnMain;
- public SettingsValues(final SharedPreferences prefs, final Context context) {
+ public SettingsValues(final SharedPreferences prefs, final InputAttributes inputAttributes,
+ final Context context) {
final Resources res = context.getResources();
// Get the resources
@@ -109,6 +128,13 @@ public class SettingsValues {
mSymbolsExcludedFromWordSeparators, res);
mHintToSaveText = context.getText(R.string.hint_add_to_dictionary);
+ // Store the input attributes
+ if (null == inputAttributes) {
+ mInputAttributes = new InputAttributes(null, false /* isFullscreenMode */);
+ } else {
+ mInputAttributes = inputAttributes;
+ }
+
// Get the settings preferences
mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, true);
mVibrateOn = isVibrateOn(context, prefs, res);
@@ -125,18 +151,13 @@ public class SettingsValues {
mUsabilityStudyMode = getUsabilityStudyMode(prefs);
mIncludesOtherImesInLanguageSwitchList = prefs.getBoolean(
Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false);
- mIsLanguageSwitchKeySuppressed = isLanguageSwitchKeySupressed(prefs);
+ mShowsLanguageSwitchKey = showsLanguageSwitchKey(prefs);
mKeyPreviewPopupDismissDelayRawValue = prefs.getString(
Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
Integer.toString(res.getInteger(R.integer.config_key_preview_linger_timeout)));
mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
mAutoCorrectEnabled = isAutoCorrectEnabled(res, mAutoCorrectionThresholdRawValue);
- mBigramSuggestionEnabled = mAutoCorrectEnabled
- && isBigramSuggestionEnabled(prefs, res, mAutoCorrectEnabled);
- mBigramPredictionEnabled = mBigramSuggestionEnabled
- && isBigramPredictionEnabled(prefs, res);
- // TODO: remove mEnableSuggestionSpanInsertion. It's always true.
- mEnableSuggestionSpanInsertion = true;
+ mBigramPredictionEnabled = isBigramPredictionEnabled(prefs, res);
mVibrationDurationSettingsRawValue =
prefs.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1);
mKeypressSoundVolumeRawValue = prefs.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f);
@@ -151,22 +172,30 @@ public class SettingsValues {
mVoiceKeyOnMain = mVoiceMode != null && mVoiceMode.equals(voiceModeMain);
mAdditionalSubtypes = AdditionalSubtype.createAdditionalSubtypesArray(
getPrefAdditionalSubtypes(prefs, res));
+ final boolean gestureInputEnabledByBuildConfig = res.getBoolean(
+ R.bool.config_gesture_input_enabled_by_build_config);
+ mGestureInputEnabled = gestureInputEnabledByBuildConfig
+ && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, true);
+ mGesturePreviewTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, true);
+ mGestureFloatingPreviewTextEnabled = prefs.getBoolean(
+ Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, true);
+ mCorrectionEnabled = mAutoCorrectEnabled && !mInputAttributes.mInputTypeNoAutoCorrect;
+ mSuggestionVisibility = createSuggestionVisibility(res);
}
// Helper functions to create member values.
private static SuggestedWords createSuggestPuncList(final String[] puncs) {
- final ArrayList<SuggestedWords.SuggestedWordInfo> puncList =
- new ArrayList<SuggestedWords.SuggestedWordInfo>();
+ final ArrayList<SuggestedWordInfo> puncList = CollectionUtils.newArrayList();
if (puncs != null) {
for (final String puncSpec : puncs) {
- puncList.add(new SuggestedWords.SuggestedWordInfo(
- KeySpecParser.getLabel(puncSpec), SuggestedWordInfo.MAX_SCORE));
+ puncList.add(new SuggestedWordInfo(KeySpecParser.getLabel(puncSpec),
+ SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_HARDCODED,
+ Dictionary.TYPE_HARDCODED));
}
}
return new SuggestedWords(puncList,
false /* typedWordValid */,
false /* hasAutoCorrectionCandidate */,
- false /* allowsToBeAutoCorrected */,
true /* isPunctuationSuggestions */,
false /* isObsoleteSuggestions */,
false /* isPrediction */);
@@ -184,6 +213,16 @@ public class SettingsValues {
return wordSeparators;
}
+ private int createSuggestionVisibility(final Resources res) {
+ final String suggestionVisiblityStr = mShowSuggestionsSetting;
+ for (int visibility : SUGGESTION_VISIBILITY_VALUE_ARRAY) {
+ if (suggestionVisiblityStr.equals(res.getString(visibility))) {
+ return visibility;
+ }
+ }
+ throw new RuntimeException("Bug: visibility string is not configured correctly");
+ }
+
private static boolean isVibrateOn(final Context context, final SharedPreferences prefs,
final Resources res) {
final boolean hasVibrator = VibratorUtils.getInstance(context).hasVibrator();
@@ -191,70 +230,83 @@ public class SettingsValues {
res.getBoolean(R.bool.config_default_vibration_enabled));
}
- public boolean isWordSeparator(int code) {
+ public boolean isApplicationSpecifiedCompletionsOn() {
+ return mInputAttributes.mApplicationSpecifiedCompletionOn;
+ }
+
+ public boolean isSuggestionsRequested(final int displayOrientation) {
+ return mInputAttributes.mIsSettingsSuggestionStripOn
+ && (mCorrectionEnabled
+ || isSuggestionStripVisibleInOrientation(displayOrientation));
+ }
+
+ public boolean isSuggestionStripVisibleInOrientation(final int orientation) {
+ return (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_VALUE)
+ || (mSuggestionVisibility == SUGGESTION_VISIBILITY_SHOW_ONLY_PORTRAIT_VALUE
+ && orientation == Configuration.ORIENTATION_PORTRAIT);
+ }
+
+ public boolean isWordSeparator(final int code) {
return mWordSeparators.contains(String.valueOf((char)code));
}
- public boolean isSymbolExcludedFromWordSeparators(int code) {
+ public boolean isSymbolExcludedFromWordSeparators(final int code) {
return mSymbolsExcludedFromWordSeparators.contains(String.valueOf((char)code));
}
- public boolean isWeakSpaceStripper(int code) {
+ // TODO: use "Phantom" instead of "Weak" in this method name
+ public boolean isWeakSpaceStripper(final int code) {
// TODO: this does not work if the code does not fit in a char
return mWeakSpaceStrippers.contains(String.valueOf((char)code));
}
- public boolean isWeakSpaceSwapper(int code) {
+ // TODO: use "Phantom" instead of "Weak" in this method name
+ public boolean isWeakSpaceSwapper(final int code) {
// TODO: this does not work if the code does not fit in a char
return mWeakSpaceSwappers.contains(String.valueOf((char)code));
}
- public boolean isPhantomSpacePromotingSymbol(int code) {
+ public boolean isPhantomSpacePromotingSymbol(final int code) {
// TODO: this does not work if the code does not fit in a char
return mPhantomSpacePromotingSymbols.contains(String.valueOf((char)code));
}
- private static boolean isAutoCorrectEnabled(final Resources resources,
+ private static boolean isAutoCorrectEnabled(final Resources res,
final String currentAutoCorrectionSetting) {
- final String autoCorrectionOff = resources.getString(
+ final String autoCorrectionOff = res.getString(
R.string.auto_correction_threshold_mode_index_off);
return !currentAutoCorrectionSetting.equals(autoCorrectionOff);
}
// Public to access from KeyboardSwitcher. Should it have access to some
// process-global instance instead?
- public static boolean isKeyPreviewPopupEnabled(SharedPreferences sp, Resources resources) {
- final boolean showPopupOption = resources.getBoolean(
+ public static boolean isKeyPreviewPopupEnabled(final SharedPreferences prefs,
+ final Resources res) {
+ final boolean showPopupOption = res.getBoolean(
R.bool.config_enable_show_popup_on_keypress_option);
- if (!showPopupOption) return resources.getBoolean(R.bool.config_default_popup_preview);
- return sp.getBoolean(Settings.PREF_POPUP_ON,
- resources.getBoolean(R.bool.config_default_popup_preview));
+ if (!showPopupOption) return res.getBoolean(R.bool.config_default_popup_preview);
+ return prefs.getBoolean(Settings.PREF_POPUP_ON,
+ res.getBoolean(R.bool.config_default_popup_preview));
}
// Likewise
- public static int getKeyPreviewPopupDismissDelay(SharedPreferences sp,
- Resources resources) {
+ public static int getKeyPreviewPopupDismissDelay(final SharedPreferences prefs,
+ final Resources res) {
// TODO: use mKeyPreviewPopupDismissDelayRawValue instead of reading it again here.
- return Integer.parseInt(sp.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
- Integer.toString(resources.getInteger(
+ return Integer.parseInt(prefs.getString(Settings.PREF_KEY_PREVIEW_POPUP_DISMISS_DELAY,
+ Integer.toString(res.getInteger(
R.integer.config_key_preview_linger_timeout))));
}
- private static boolean isBigramSuggestionEnabled(final SharedPreferences sp,
- final Resources resources, final boolean autoCorrectEnabled) {
- // TODO: remove this method. Bigram suggestion is always true.
- return true;
- }
-
- private static boolean isBigramPredictionEnabled(final SharedPreferences sp,
- final Resources resources) {
- return sp.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, resources.getBoolean(
+ private static boolean isBigramPredictionEnabled(final SharedPreferences prefs,
+ final Resources res) {
+ return prefs.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, res.getBoolean(
R.bool.config_default_next_word_prediction));
}
- private static float getAutoCorrectionThreshold(final Resources resources,
+ private static float getAutoCorrectionThreshold(final Resources res,
final String currentAutoCorrectionSetting) {
- final String[] autoCorrectionThresholdValues = resources.getStringArray(
+ final String[] autoCorrectionThresholdValues = res.getStringArray(
R.array.auto_correction_threshold_values);
// When autoCorrectionThreshold is greater than 1.0, it's like auto correction is off.
float autoCorrectionThreshold = Float.MAX_VALUE;
@@ -286,12 +338,25 @@ public class SettingsValues {
return mVoiceKeyOnMain;
}
- public static boolean isLanguageSwitchKeySupressed(SharedPreferences sp) {
- return sp.getBoolean(Settings.PREF_SUPPRESS_LANGUAGE_SWITCH_KEY, false);
+ // This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead.
+ // This is being used only for the backward compatibility.
+ private static final String PREF_SUPPRESS_LANGUAGE_SWITCH_KEY =
+ "pref_suppress_language_switch_key";
+
+ public static boolean showsLanguageSwitchKey(final SharedPreferences prefs) {
+ if (prefs.contains(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY)) {
+ final boolean suppressLanguageSwitchKey = prefs.getBoolean(
+ PREF_SUPPRESS_LANGUAGE_SWITCH_KEY, false);
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.remove(PREF_SUPPRESS_LANGUAGE_SWITCH_KEY);
+ editor.putBoolean(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY, !suppressLanguageSwitchKey);
+ editor.apply();
+ }
+ return prefs.getBoolean(Settings.PREF_SHOW_LANGUAGE_SWITCH_KEY, true);
}
- public boolean isLanguageSwitchKeyEnabled(Context context) {
- if (mIsLanguageSwitchKeySuppressed) {
+ public boolean isLanguageSwitchKeyEnabled(final Context context) {
+ if (!mShowsLanguageSwitchKey) {
return false;
}
if (mIncludesOtherImesInLanguageSwitchList) {
@@ -303,7 +368,7 @@ public class SettingsValues {
}
}
- public boolean isFullscreenModeAllowed(Resources res) {
+ public static boolean isFullscreenModeAllowed(final Resources res) {
return res.getBoolean(R.bool.config_use_fullscreen_mode);
}
@@ -313,58 +378,68 @@ public class SettingsValues {
public static String getPrefAdditionalSubtypes(final SharedPreferences prefs,
final Resources res) {
- final String prefSubtypes = res.getString(R.string.predefined_subtypes, "");
- return prefs.getString(Settings.PREF_CUSTOM_INPUT_STYLES, prefSubtypes);
+ final String predefinedPrefSubtypes = AdditionalSubtype.createPrefSubtypes(
+ res.getStringArray(R.array.predefined_subtypes));
+ return prefs.getString(Settings.PREF_CUSTOM_INPUT_STYLES, predefinedPrefSubtypes);
}
// Accessed from the settings interface, hence public
- public static float getCurrentKeypressSoundVolume(final SharedPreferences sp,
- final Resources res) {
+ public static float getCurrentKeypressSoundVolume(final SharedPreferences prefs,
+ final Resources res) {
// TODO: use mVibrationDurationSettingsRawValue instead of reading it again here
- final float volume = sp.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f);
+ final float volume = prefs.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f);
if (volume >= 0) {
return volume;
}
- return Float.parseFloat(
- Utils.getDeviceOverrideValue(res, R.array.keypress_volumes, "-1.0f"));
+ return Float.parseFloat(ResourceUtils.getDeviceOverrideValue(
+ res, R.array.keypress_volumes, "-1.0f"));
}
// Likewise
- public static int getCurrentVibrationDuration(final SharedPreferences sp,
- final Resources res) {
+ public static int getCurrentVibrationDuration(final SharedPreferences prefs,
+ final Resources res) {
// TODO: use mKeypressVibrationDuration instead of reading it again here
- final int ms = sp.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1);
+ final int ms = prefs.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1);
if (ms >= 0) {
return ms;
}
- return Integer.parseInt(
- Utils.getDeviceOverrideValue(res, R.array.keypress_vibration_durations, "-1"));
+ return Integer.parseInt(ResourceUtils.getDeviceOverrideValue(
+ res, R.array.keypress_vibration_durations, "-1"));
}
// Likewise
public static boolean getUsabilityStudyMode(final SharedPreferences prefs) {
// TODO: use mUsabilityStudyMode instead of reading it again here
- return prefs.getBoolean(Settings.PREF_USABILITY_STUDY_MODE, true);
+ return prefs.getBoolean(DebugSettings.PREF_USABILITY_STUDY_MODE, true);
}
- public static long getLastUserHistoryWriteTime(
- final SharedPreferences prefs, final String locale) {
+ public static long getLastUserHistoryWriteTime(final SharedPreferences prefs,
+ final String locale) {
final String str = prefs.getString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, "");
- final HashMap<String, Long> map = Utils.localeAndTimeStrToHashMap(str);
+ final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(str);
if (map.containsKey(locale)) {
return map.get(locale);
}
return 0;
}
- public static void setLastUserHistoryWriteTime(
- final SharedPreferences prefs, final String locale) {
+ public static void setLastUserHistoryWriteTime(final SharedPreferences prefs,
+ final String locale) {
final String oldStr = prefs.getString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, "");
- final HashMap<String, Long> map = Utils.localeAndTimeStrToHashMap(oldStr);
+ final HashMap<String, Long> map = LocaleUtils.localeAndTimeStrToHashMap(oldStr);
map.put(locale, System.currentTimeMillis());
- final String newStr = Utils.localeAndTimeHashMapToStr(map);
+ final String newStr = LocaleUtils.localeAndTimeHashMapToStr(map);
prefs.edit().putString(Settings.PREF_LAST_USER_DICTIONARY_WRITE_TIME, newStr).apply();
}
+
+ public boolean isSameInputType(final EditorInfo editorInfo) {
+ return mInputAttributes.isSameInputType(editorInfo);
+ }
+
+ // For debug.
+ public String getInputAttributesDebugString() {
+ return mInputAttributes.toString();
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java
index a43b90525..df7709892 100644
--- a/java/src/com/android/inputmethod/latin/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/StringUtils.java
@@ -18,10 +18,12 @@ package com.android.inputmethod.latin;
import android.text.TextUtils;
+import com.android.inputmethod.keyboard.Keyboard; // For character constants
+
import java.util.ArrayList;
import java.util.Locale;
-public class StringUtils {
+public final class StringUtils {
private StringUtils() {
// This utility class is not publicly instantiable.
}
@@ -53,7 +55,7 @@ public class StringUtils {
if (TextUtils.isEmpty(csv)) return "";
final String[] elements = csv.split(",");
if (!containsInArray(key, elements)) return csv;
- final ArrayList<String> result = new ArrayList<String>(elements.length - 1);
+ final ArrayList<String> result = CollectionUtils.newArrayList(elements.length - 1);
for (final String element : elements) {
if (!key.equals(element)) result.add(element);
}
@@ -123,23 +125,6 @@ public class StringUtils {
}
/**
- * Returns true if cs contains any upper case characters.
- *
- * @param cs the CharSequence to check
- * @return {@code true} if cs contains any upper case characters, {@code false} otherwise.
- */
- public static boolean hasUpperCase(final CharSequence cs) {
- final int length = cs.length();
- for (int i = 0, cp = 0; i < length; i += Character.charCount(cp)) {
- cp = Character.codePointAt(cs, i);
- if (Character.isUpperCase(cp)) {
- return true;
- }
- }
- return false;
- }
-
- /**
* Remove duplicates from an array of strings.
*
* This method will always keep the first occurrence of all strings at their position
@@ -184,6 +169,9 @@ public class StringUtils {
final char[] characters = string.toCharArray();
final int length = characters.length;
final int[] codePoints = new int[Character.codePointCount(characters, 0, length)];
+ if (length <= 0) {
+ return new int[0];
+ }
int codePoint = Character.codePointAt(characters, 0);
int dsti = 0;
for (int srci = Character.charCount(codePoint);
@@ -194,4 +182,211 @@ public class StringUtils {
codePoints[dsti] = codePoint;
return codePoints;
}
+
+ /**
+ * 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
+ * checked. Note that the caps mode flags here are explicitly defined
+ * to match those in {@link InputType}.
+ *
+ * This code is a straight copy of TextUtils.getCapsMode (modulo namespace and formatting
+ * issues). This will change in the future as we simplify the code for our use and fix bugs.
+ *
+ * @param cs The text that should be checked for caps modes.
+ * @param reqModes The modes to be checked: may be any combination of
+ * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
+ * {@link TextUtils#CAP_MODE_SENTENCES}.
+ * @param locale The locale to consider for capitalization rules
+ * @param hasSpaceBefore Whether we should consider there is a space inserted at the end of cs
+ *
+ * @return Returns the actual capitalization modes that can be in effect
+ * at the current position, which is any combination of
+ * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
+ * {@link TextUtils#CAP_MODE_SENTENCES}.
+ */
+ public static int getCapsMode(final CharSequence cs, final int reqModes, final Locale locale,
+ final boolean hasSpaceBefore) {
+ // Quick description of what we want to do:
+ // CAP_MODE_CHARACTERS is always on.
+ // CAP_MODE_WORDS is on if there is some whitespace before the cursor.
+ // CAP_MODE_SENTENCES is on if there is some whitespace before the cursor, and the end
+ // of a sentence just before that.
+ // We ignore opening parentheses and the like just before the cursor for purposes of
+ // finding whitespace for WORDS and SENTENCES modes.
+ // The end of a sentence ends with a period, question mark or exclamation mark. If it's
+ // a period, it also needs not to be an abbreviation, which means it also needs to either
+ // be immediately preceded by punctuation, or by a string of only letters with single
+ // periods interleaved.
+
+ // Step 1 : check for cap MODE_CHARACTERS. If it's looked for, it's always on.
+ if ((reqModes & (TextUtils.CAP_MODE_WORDS | TextUtils.CAP_MODE_SENTENCES)) == 0) {
+ // Here we are not looking for MODE_WORDS or MODE_SENTENCES, so since we already
+ // evaluated MODE_CHARACTERS, we can return.
+ return TextUtils.CAP_MODE_CHARACTERS & reqModes;
+ }
+
+ // Step 2 : Skip (ignore at the end of input) any opening punctuation. This includes
+ // opening parentheses, brackets, opening quotes, everything that *opens* a span of
+ // text in the linguistic sense. In RTL languages, this is still an opening sign, although
+ // it may look like a right parenthesis for example. We also include double quote and
+ // single quote since they aren't start punctuation in the unicode sense, but should still
+ // be skipped for English. TODO: does this depend on the language?
+ int i;
+ if (hasSpaceBefore) {
+ i = cs.length() + 1;
+ } else {
+ for (i = cs.length(); i > 0; i--) {
+ final char c = cs.charAt(i - 1);
+ if (c != Keyboard.CODE_DOUBLE_QUOTE && c != Keyboard.CODE_SINGLE_QUOTE
+ && Character.getType(c) != Character.START_PUNCTUATION) {
+ break;
+ }
+ }
+ }
+
+ // We are now on the character that precedes any starting punctuation, so in the most
+ // frequent case this will be whitespace or a letter, although it may occasionally be a
+ // start of line, or some symbol.
+
+ // Step 3 : Search for the start of a paragraph. From the starting point computed in step 2,
+ // we go back over any space or tab char sitting there. We find the start of a paragraph
+ // if the first char that's not a space or tab is a start of line (as in \n, start of text,
+ // or some other similar characters).
+ int j = i;
+ char prevChar = Keyboard.CODE_SPACE;
+ if (hasSpaceBefore) --j;
+ while (j > 0) {
+ prevChar = cs.charAt(j - 1);
+ if (!Character.isSpaceChar(prevChar) && prevChar != Keyboard.CODE_TAB) break;
+ j--;
+ }
+ if (j <= 0 || Character.isWhitespace(prevChar)) {
+ // There are only spacing chars between the start of the paragraph and the cursor,
+ // defined as a isWhitespace() char that is neither a isSpaceChar() nor a tab. Both
+ // MODE_WORDS and MODE_SENTENCES should be active.
+ return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
+ | TextUtils.CAP_MODE_SENTENCES) & reqModes;
+ }
+ if (i == j) {
+ // If we don't have whitespace before index i, it means neither MODE_WORDS
+ // nor mode sentences should be on so we can return right away.
+ return TextUtils.CAP_MODE_CHARACTERS & reqModes;
+ }
+ if ((reqModes & TextUtils.CAP_MODE_SENTENCES) == 0) {
+ // Here we know we have whitespace before the cursor (if not, we returned in the above
+ // if i == j clause), so we need MODE_WORDS to be on. And we don't need to evaluate
+ // MODE_SENTENCES so we can return right away.
+ return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
+ }
+ // Please note that because of the reqModes & CAP_MODE_SENTENCES test a few lines above,
+ // we know that MODE_SENTENCES is being requested.
+
+ // Step 4 : Search for MODE_SENTENCES.
+ // English is a special case in that "American typography" rules, which are the most common
+ // in English, state that a sentence terminator immediately following a quotation mark
+ // should be swapped with it and de-duplicated (included in the quotation mark),
+ // e.g. <<Did he say, "let's go home?">>
+ // No other language has such a rule as far as I know, instead putting inside the quotation
+ // mark as the exact thing quoted and handling the surrounding punctuation independently,
+ // e.g. <<Did he say, "let's go home"?>>
+ // Hence, specifically for English, we treat this special case here.
+ if (Locale.ENGLISH.getLanguage().equals(locale.getLanguage())) {
+ for (; j > 0; j--) {
+ // Here we look to go over any closing punctuation. This is because in dominant
+ // variants of English, the final period is placed within double quotes and maybe
+ // other closing punctuation signs. This is generally not true in other languages.
+ final char c = cs.charAt(j - 1);
+ if (c != Keyboard.CODE_DOUBLE_QUOTE && c != Keyboard.CODE_SINGLE_QUOTE
+ && Character.getType(c) != Character.END_PUNCTUATION) {
+ break;
+ }
+ }
+ }
+
+ if (j <= 0) return TextUtils.CAP_MODE_CHARACTERS & reqModes;
+ char c = cs.charAt(--j);
+
+ // We found the next interesting chunk of text ; next we need to determine if it's the
+ // end of a sentence. If we have a question mark or an exclamation mark, it's the end of
+ // a sentence. If it's neither, the only remaining case is the period so we get the opposite
+ // case out of the way.
+ if (c == Keyboard.CODE_QUESTION_MARK || c == Keyboard.CODE_EXCLAMATION_MARK) {
+ return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_SENTENCES) & reqModes;
+ }
+ if (c != Keyboard.CODE_PERIOD || j <= 0) {
+ return (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
+ }
+
+ // We found out that we have a period. We need to determine if this is a full stop or
+ // otherwise sentence-ending period, or an abbreviation like "e.g.". An abbreviation
+ // looks like (\w\.){2,}
+ // To find out, we will have a simple state machine with the following states :
+ // START, WORD, PERIOD, ABBREVIATION
+ // On START : (just before the first period)
+ // letter => WORD
+ // whitespace => end with no caps (it was a stand-alone period)
+ // otherwise => end with caps (several periods/symbols in a row)
+ // On WORD : (within the word just before the first period)
+ // letter => WORD
+ // period => PERIOD
+ // otherwise => end with caps (it was a word with a full stop at the end)
+ // On PERIOD : (period within a potential abbreviation)
+ // letter => LETTER
+ // otherwise => end with caps (it was not an abbreviation)
+ // On LETTER : (letter within a potential abbreviation)
+ // letter => LETTER
+ // period => PERIOD
+ // otherwise => end with no caps (it was an abbreviation)
+ // "Not an abbreviation" in the above chart essentially covers cases like "...yes.". This
+ // should capitalize.
+
+ final int START = 0;
+ final int WORD = 1;
+ final int PERIOD = 2;
+ final int LETTER = 3;
+ final int caps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
+ | TextUtils.CAP_MODE_SENTENCES) & reqModes;
+ final int noCaps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
+ int state = START;
+ while (j > 0) {
+ c = cs.charAt(--j);
+ switch (state) {
+ case START:
+ if (Character.isLetter(c)) {
+ state = WORD;
+ } else if (Character.isWhitespace(c)) {
+ return noCaps;
+ } else {
+ return caps;
+ }
+ break;
+ case WORD:
+ if (Character.isLetter(c)) {
+ state = WORD;
+ } else if (c == Keyboard.CODE_PERIOD) {
+ state = PERIOD;
+ } else {
+ return caps;
+ }
+ break;
+ case PERIOD:
+ if (Character.isLetter(c)) {
+ state = LETTER;
+ } else {
+ return caps;
+ }
+ break;
+ case LETTER:
+ if (Character.isLetter(c)) {
+ state = LETTER;
+ } else if (c == Keyboard.CODE_PERIOD) {
+ state = PERIOD;
+ } else {
+ return noCaps;
+ }
+ }
+ }
+ // Here we arrived at the start of the line. This should behave exactly like whitespace.
+ return (START == state || LETTER == state) ? noCaps : caps;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
index 21c9c0d1e..579f96bb4 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
@@ -30,7 +30,7 @@ import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
import java.util.HashMap;
import java.util.Locale;
-public class SubtypeLocale {
+public final class SubtypeLocale {
static final String TAG = SubtypeLocale.class.getSimpleName();
// This class must be located in the same package as LatinIME.java.
private static final String RESOURCE_PACKAGE_NAME =
@@ -45,13 +45,13 @@ public class SubtypeLocale {
private static String[] sPredefinedKeyboardLayoutSet;
// Keyboard layout to its display name map.
private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap =
- new HashMap<String, String>();
+ CollectionUtils.newHashMap();
// Keyboard layout to subtype name resource id map.
private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap =
- new HashMap<String, Integer>();
+ CollectionUtils.newHashMap();
// Exceptional locale to subtype name resource id map.
private static final HashMap<String, Integer> sExceptionalLocaleToWithLayoutNameIdsMap =
- new HashMap<String, Integer>();
+ CollectionUtils.newHashMap();
private static final String SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX =
"string/subtype_generic_";
private static final String SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX =
@@ -60,11 +60,11 @@ public class SubtypeLocale {
"string/subtype_no_language_";
// Exceptional locales to display name map.
private static final HashMap<String, String> sExceptionalDisplayNamesMap =
- new HashMap<String, String>();
+ CollectionUtils.newHashMap();
// Keyboard layout set name for the subtypes that don't have a keyboardLayoutSet extra value.
// This is for compatibility to keep the same subtype ids as pre-JellyBean.
private static final HashMap<String,String> sLocaleAndExtraValueToKeyboardLayoutSetMap =
- new HashMap<String,String>();
+ CollectionUtils.newHashMap();
private SubtypeLocale() {
// Intentional empty constructor for utility class.
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index 664de6774..8e51a372b 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -22,6 +22,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.inputmethodservice.InputMethodService;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
@@ -37,12 +38,11 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
-public class SubtypeSwitcher {
+public final class SubtypeSwitcher {
private static boolean DBG = LatinImeLogger.sDBG;
private static final String TAG = SubtypeSwitcher.class.getSimpleName();
private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
- private /* final */ LatinIME mService;
private /* final */ InputMethodManager mImm;
private /* final */ Resources mResources;
private /* final */ ConnectivityManager mConnectivityManager;
@@ -60,7 +60,7 @@ public class SubtypeSwitcher {
private boolean mIsNetworkConnected;
- static class NeedsToDisplayLanguage {
+ static final class NeedsToDisplayLanguage {
private int mEnabledSubtypeCount;
private boolean mIsSystemLanguageSameAsInputLanguage;
@@ -68,11 +68,11 @@ public class SubtypeSwitcher {
return mEnabledSubtypeCount >= 2 || !mIsSystemLanguageSameAsInputLanguage;
}
- public void updateEnabledSubtypeCount(int count) {
+ public void updateEnabledSubtypeCount(final int count) {
mEnabledSubtypeCount = count;
}
- public void updateIsSystemLanguageSameAsInputLanguage(boolean isSame) {
+ public void updateIsSystemLanguageSameAsInputLanguage(final boolean isSame) {
mIsSystemLanguageSameAsInputLanguage = isSame;
}
}
@@ -81,26 +81,25 @@ public class SubtypeSwitcher {
return sInstance;
}
- public static void init(LatinIME service) {
- SubtypeLocale.init(service);
- sInstance.initialize(service);
- sInstance.updateAllParameters();
+ public static void init(final Context context) {
+ SubtypeLocale.init(context);
+ sInstance.initialize(context);
+ sInstance.updateAllParameters(context);
}
private SubtypeSwitcher() {
// Intentional empty constructor for singleton.
}
- private void initialize(LatinIME service) {
- mService = service;
+ private void initialize(final Context service) {
mResources = service.getResources();
mImm = ImfUtils.getInputMethodManager(service);
mConnectivityManager = (ConnectivityManager) service.getSystemService(
Context.CONNECTIVITY_SERVICE);
mCurrentSystemLocale = mResources.getConfiguration().locale;
- mCurrentSubtype = mImm.getCurrentInputMethodSubtype();
mNoLanguageSubtype = ImfUtils.findSubtypeByLocaleAndKeyboardLayoutSet(
service, SubtypeLocale.NO_LANGUAGE, SubtypeLocale.QWERTY);
+ mCurrentSubtype = ImfUtils.getCurrentInputMethodSubtype(service, mNoLanguageSubtype);
if (mNoLanguageSubtype == null) {
throw new RuntimeException("Can't find no lanugage with QWERTY subtype");
}
@@ -111,39 +110,46 @@ public class SubtypeSwitcher {
// Update all parameters stored in SubtypeSwitcher.
// Only configuration changed event is allowed to call this because this is heavy.
- private void updateAllParameters() {
+ private void updateAllParameters(final Context context) {
mCurrentSystemLocale = mResources.getConfiguration().locale;
- updateSubtype(mImm.getCurrentInputMethodSubtype());
- updateParametersOnStartInputView();
+ updateSubtype(ImfUtils.getCurrentInputMethodSubtype(context, mNoLanguageSubtype));
+ updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled();
}
- // Update parameters which are changed outside LatinIME. This parameters affect UI so they
- // should be updated every time onStartInputview.
- public void updateParametersOnStartInputView() {
- updateEnabledSubtypes();
+ /**
+ * Update parameters which are changed outside LatinIME. This parameters affect UI so they
+ * should be updated every time onStartInputView.
+ *
+ * @return true if the current subtype is enabled.
+ */
+ public boolean updateParametersOnStartInputViewAndReturnIfCurrentSubtypeEnabled() {
+ final boolean currentSubtypeEnabled =
+ updateEnabledSubtypesAndReturnIfEnabled(mCurrentSubtype);
updateShortcutIME();
+ return currentSubtypeEnabled;
}
- // Reload enabledSubtypes from the framework.
- private void updateEnabledSubtypes() {
- final InputMethodSubtype currentSubtype = mCurrentSubtype;
- boolean foundCurrentSubtypeBecameDisabled = true;
+ /**
+ * Update enabled subtypes from the framework.
+ *
+ * @param subtype the subtype to be checked
+ * @return true if the {@code subtype} is enabled.
+ */
+ private boolean updateEnabledSubtypesAndReturnIfEnabled(final InputMethodSubtype subtype) {
final List<InputMethodSubtype> enabledSubtypesOfThisIme =
mImm.getEnabledInputMethodSubtypeList(null, true);
- for (InputMethodSubtype ims : enabledSubtypesOfThisIme) {
- if (ims.equals(currentSubtype)) {
- foundCurrentSubtypeBecameDisabled = false;
- }
- }
mNeedsToDisplayLanguage.updateEnabledSubtypeCount(enabledSubtypesOfThisIme.size());
- if (foundCurrentSubtypeBecameDisabled) {
- if (DBG) {
- Log.w(TAG, "Last subtype: "
- + currentSubtype.getLocale() + "/" + currentSubtype.getExtraValue());
- Log.w(TAG, "Last subtype was disabled. Update to the current one.");
+
+ for (final InputMethodSubtype ims : enabledSubtypesOfThisIme) {
+ if (ims.equals(subtype)) {
+ return true;
}
- updateSubtype(mImm.getCurrentInputMethodSubtype());
}
+ if (DBG) {
+ Log.w(TAG, "Subtype: " + subtype.getLocale() + "/" + subtype.getExtraValue()
+ + " was disabled");
+ }
+ return false;
}
private void updateShortcutIME() {
@@ -159,8 +165,8 @@ public class SubtypeSwitcher {
mImm.getShortcutInputMethodsAndSubtypes();
mShortcutInputMethodInfo = null;
mShortcutSubtype = null;
- for (InputMethodInfo imi : shortcuts.keySet()) {
- List<InputMethodSubtype> subtypes = shortcuts.get(imi);
+ for (final InputMethodInfo imi : shortcuts.keySet()) {
+ final List<InputMethodSubtype> subtypes = shortcuts.get(imi);
// TODO: Returns the first found IMI for now. Should handle all shortcuts as
// appropriate.
mShortcutInputMethodInfo = imi;
@@ -194,24 +200,24 @@ public class SubtypeSwitcher {
mCurrentSubtype = newSubtype;
updateShortcutIME();
- mService.onRefreshKeyboard();
}
////////////////////////////
// Shortcut IME functions //
////////////////////////////
- public void switchToShortcutIME() {
+ public void switchToShortcutIME(final InputMethodService context) {
if (mShortcutInputMethodInfo == null) {
return;
}
final String imiId = mShortcutInputMethodInfo.getId();
- switchToTargetIME(imiId, mShortcutSubtype);
+ switchToTargetIME(imiId, mShortcutSubtype, context);
}
- private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype) {
- final IBinder token = mService.getWindow().getWindow().getAttributes().token;
+ private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype,
+ final InputMethodService context) {
+ final IBinder token = context.getWindow().getWindow().getAttributes().token;
if (token == null) {
return;
}
@@ -253,7 +259,7 @@ public class SubtypeSwitcher {
return true;
}
- public void onNetworkStateChanged(Intent intent) {
+ public void onNetworkStateChanged(final Intent intent) {
final boolean noConnection = intent.getBooleanExtra(
ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
mIsNetworkConnected = !noConnection;
@@ -265,7 +271,7 @@ public class SubtypeSwitcher {
// Subtype Switching functions //
//////////////////////////////////
- public boolean needsToDisplayLanguage(Locale keyboardLocale) {
+ public boolean needsToDisplayLanguage(final Locale keyboardLocale) {
if (keyboardLocale.toString().equals(SubtypeLocale.NO_LANGUAGE)) {
return true;
}
@@ -279,12 +285,14 @@ public class SubtypeSwitcher {
return SubtypeLocale.getSubtypeLocale(mCurrentSubtype);
}
- public void onConfigurationChanged(Configuration conf) {
+ public boolean onConfigurationChanged(final Configuration conf, final Context context) {
final Locale systemLocale = conf.locale;
+ final boolean systemLocaleChanged = !systemLocale.equals(mCurrentSystemLocale);
// If system configuration was changed, update all parameters.
- if (!systemLocale.equals(mCurrentSystemLocale)) {
- updateAllParameters();
+ if (systemLocaleChanged) {
+ updateAllParameters(context);
}
+ return systemLocaleChanged;
}
public InputMethodSubtype getCurrentSubtype() {
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 336a76f4b..f0e3b4ebd 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -18,7 +18,6 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.text.TextUtils;
-import android.util.Log;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.ProximityInfo;
@@ -26,6 +25,7 @@ import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import java.io.File;
import java.util.ArrayList;
+import java.util.Comparator;
import java.util.HashSet;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
@@ -34,87 +34,55 @@ import java.util.concurrent.ConcurrentHashMap;
* This class loads a dictionary and provides a list of suggestions for a given sequence of
* characters. This includes corrections and completions.
*/
-public class Suggest implements Dictionary.WordCallback {
+public final class Suggest {
public static final String TAG = Suggest.class.getSimpleName();
- public static final int APPROX_MAX_WORD_LENGTH = 32;
+ // Session id for
+ // {@link #getSuggestedWords(WordComposer,CharSequence,ProximityInfo,boolean,int)}.
+ public static final int SESSION_TYPING = 0;
+ public static final int SESSION_GESTURE = 1;
+ // TODO: rename this to CORRECTION_OFF
public static final int CORRECTION_NONE = 0;
+ // TODO: rename this to CORRECTION_ON
public static final int CORRECTION_FULL = 1;
- public static final int CORRECTION_FULL_BIGRAM = 2;
-
- // It seems the following values are only used for logging.
- public static final int DIC_USER_TYPED = 0;
- public static final int DIC_MAIN = 1;
- public static final int DIC_USER = 2;
- public static final int DIC_USER_HISTORY = 3;
- public static final int DIC_CONTACTS = 4;
- public static final int DIC_WHITELIST = 6;
- // If you add a type of dictionary, increment DIC_TYPE_LAST_ID
- // TODO: this value seems unused. Remove it?
- public static final int DIC_TYPE_LAST_ID = 6;
- public static final String DICT_KEY_MAIN = "main";
- public static final String DICT_KEY_CONTACTS = "contacts";
- // User dictionary, the system-managed one.
- public static final String DICT_KEY_USER = "user";
- // User history dictionary for the unigram map, internal to LatinIME
- public static final String DICT_KEY_USER_HISTORY_UNIGRAM = "history_unigram";
- // User history dictionary for the bigram map, internal to LatinIME
- public static final String DICT_KEY_USER_HISTORY_BIGRAM = "history_bigram";
- public static final String DICT_KEY_WHITELIST ="whitelist";
- private static final boolean DBG = LatinImeLogger.sDBG;
+ public interface SuggestInitializationListener {
+ public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable);
+ }
- private boolean mHasMainDictionary;
- private Dictionary mContactsDict;
- private WhitelistDictionary mWhiteListDictionary;
- private final ConcurrentHashMap<String, Dictionary> mUnigramDictionaries =
- new ConcurrentHashMap<String, Dictionary>();
- private final ConcurrentHashMap<String, Dictionary> mBigramDictionaries =
- new ConcurrentHashMap<String, Dictionary>();
+ private static final boolean DBG = LatinImeLogger.sDBG;
- private int mPrefMaxSuggestions = 18;
+ private Dictionary mMainDictionary;
+ private ContactsBinaryDictionary mContactsDict;
+ private final ConcurrentHashMap<String, Dictionary> mDictionaries =
+ CollectionUtils.newConcurrentHashMap();
- private static final int PREF_MAX_BIGRAMS = 60;
+ public static final int MAX_SUGGESTIONS = 18;
private float mAutoCorrectionThreshold;
- private ArrayList<SuggestedWordInfo> mSuggestions = new ArrayList<SuggestedWordInfo>();
- private ArrayList<SuggestedWordInfo> mBigramSuggestions = new ArrayList<SuggestedWordInfo>();
- private CharSequence mConsideredWord;
+ // Locale used for upper- and title-casing words
+ private final Locale mLocale;
- // TODO: Remove these member variables by passing more context to addWord() callback method
- private boolean mIsFirstCharCapitalized;
- private boolean mIsAllUpperCase;
- private int mTrailingSingleQuotesCount;
-
- private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
-
- public Suggest(final Context context, final Locale locale) {
- initAsynchronously(context, locale);
+ public Suggest(final Context context, final Locale locale,
+ final SuggestInitializationListener listener) {
+ initAsynchronously(context, locale, listener);
+ mLocale = locale;
}
/* package for test */ Suggest(final Context context, final File dictionary,
final long startOffset, final long length, final Locale locale) {
final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(context, dictionary,
startOffset, length /* useFullEditDistance */, false, locale);
- mHasMainDictionary = null != mainDict;
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, mainDict);
- addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, mainDict);
- initWhitelistAndAutocorrectAndPool(context, locale);
+ mLocale = locale;
+ mMainDictionary = mainDict;
+ addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, mainDict);
}
- private void initWhitelistAndAutocorrectAndPool(final Context context, final Locale locale) {
- mWhiteListDictionary = new WhitelistDictionary(context, locale);
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_WHITELIST, mWhiteListDictionary);
- }
-
- private void initAsynchronously(final Context context, final Locale locale) {
- resetMainDict(context, locale);
-
- // TODO: read the whitelist and init the pool asynchronously too.
- // initPool should be done asynchronously now that the pool is thread-safe.
- initWhitelistAndAutocorrectAndPool(context, locale);
+ private void initAsynchronously(final Context context, final Locale locale,
+ final SuggestInitializationListener listener) {
+ resetMainDict(context, locale, listener);
}
private static void addOrReplaceDictionary(
@@ -128,16 +96,22 @@ public class Suggest implements Dictionary.WordCallback {
}
}
- public void resetMainDict(final Context context, final Locale locale) {
- mHasMainDictionary = false;
+ public void resetMainDict(final Context context, final Locale locale,
+ final SuggestInitializationListener listener) {
+ mMainDictionary = null;
+ if (listener != null) {
+ listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+ }
new Thread("InitializeBinaryDictionary") {
@Override
public void run() {
final DictionaryCollection newMainDict =
DictionaryFactory.createMainDictionaryFromManager(context, locale);
- mHasMainDictionary = null != newMainDict && !newMainDict.isEmpty();
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, newMainDict);
- addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, newMainDict);
+ addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_MAIN, newMainDict);
+ mMainDictionary = newMainDict;
+ if (listener != null) {
+ listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+ }
}
}.start();
}
@@ -145,27 +119,27 @@ public class Suggest implements Dictionary.WordCallback {
// The main dictionary could have been loaded asynchronously. Don't cache the return value
// of this method.
public boolean hasMainDictionary() {
- return mHasMainDictionary;
+ return null != mMainDictionary && mMainDictionary.isInitialized();
}
- public Dictionary getContactsDictionary() {
- return mContactsDict;
+ public Dictionary getMainDictionary() {
+ return mMainDictionary;
}
- public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() {
- return mUnigramDictionaries;
+ public ContactsBinaryDictionary getContactsDictionary() {
+ return mContactsDict;
}
- public static int getApproxMaxWordLength() {
- return APPROX_MAX_WORD_LENGTH;
+ public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() {
+ return mDictionaries;
}
/**
* Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
* before the main dictionary, if set. This refers to the system-managed user dictionary.
*/
- public void setUserDictionary(Dictionary userDictionary) {
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER, userDictionary);
+ public void setUserDictionary(UserBinaryDictionary userDictionary) {
+ addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_USER, userDictionary);
}
/**
@@ -173,236 +147,193 @@ public class Suggest implements Dictionary.WordCallback {
* the contacts dictionary by passing null to this method. In this case no contacts dictionary
* won't be used.
*/
- public void setContactsDictionary(Dictionary contactsDictionary) {
+ public void setContactsDictionary(ContactsBinaryDictionary contactsDictionary) {
mContactsDict = contactsDictionary;
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
- addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
+ addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_CONTACTS, contactsDictionary);
}
- public void setUserHistoryDictionary(Dictionary userHistoryDictionary) {
- addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER_HISTORY_UNIGRAM,
- userHistoryDictionary);
- addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_USER_HISTORY_BIGRAM,
- userHistoryDictionary);
+ public void setUserHistoryDictionary(UserHistoryDictionary userHistoryDictionary) {
+ addOrReplaceDictionary(mDictionaries, Dictionary.TYPE_USER_HISTORY, userHistoryDictionary);
}
public void setAutoCorrectionThreshold(float threshold) {
mAutoCorrectionThreshold = threshold;
}
- private static CharSequence capitalizeWord(final boolean all, final boolean first,
- final CharSequence word) {
- if (TextUtils.isEmpty(word) || !(all || first)) return word;
- final int wordLength = word.length();
- final StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
- // TODO: Must pay attention to locale when changing case.
- if (all) {
- sb.append(word.toString().toUpperCase());
- } else if (first) {
- sb.append(Character.toUpperCase(word.charAt(0)));
- if (wordLength > 1) {
- sb.append(word.subSequence(1, wordLength));
- }
- }
- return sb;
- }
-
- protected void addBigramToSuggestions(SuggestedWordInfo bigram) {
- mSuggestions.add(bigram);
- }
-
- private static final WordComposer sEmptyWordComposer = new WordComposer();
- public SuggestedWords getBigramPredictions(CharSequence prevWordForBigram) {
+ public SuggestedWords getSuggestedWords(
+ final WordComposer wordComposer, CharSequence prevWordForBigram,
+ final ProximityInfo proximityInfo, final boolean isCorrectionEnabled, int sessionId) {
LatinImeLogger.onStartSuggestion(prevWordForBigram);
- mIsFirstCharCapitalized = false;
- mIsAllUpperCase = false;
- mTrailingSingleQuotesCount = 0;
- mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions);
-
- // Treating USER_TYPED as UNIGRAM suggestion for logging now.
- LatinImeLogger.onAddSuggestedWord("", Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
- mConsideredWord = "";
-
- mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS);
-
- getAllBigrams(prevWordForBigram, sEmptyWordComposer);
-
- // Nothing entered: return all bigrams for the previous word
- int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
- for (int i = 0; i < insertCount; ++i) {
- addBigramToSuggestions(mBigramSuggestions.get(i));
+ if (wordComposer.isBatchMode()) {
+ return getSuggestedWordsForBatchInput(
+ wordComposer, prevWordForBigram, proximityInfo, sessionId);
+ } else {
+ return getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo,
+ isCorrectionEnabled);
}
-
- SuggestedWordInfo.removeDups(mSuggestions);
-
- return new SuggestedWords(mSuggestions,
- false /* typedWordValid */,
- false /* hasAutoCorrectionCandidate */,
- false /* allowsToBeAutoCorrected */,
- false /* isPunctuationSuggestions */,
- false /* isObsoleteSuggestions */,
- true /* isPrediction */);
}
- // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder
- public SuggestedWords getSuggestedWords(
+ // Retrieves suggestions for the typing input.
+ private SuggestedWords getSuggestedWordsForTypingInput(
final WordComposer wordComposer, CharSequence prevWordForBigram,
- final ProximityInfo proximityInfo, final int correctionMode) {
- LatinImeLogger.onStartSuggestion(prevWordForBigram);
- mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
- mIsAllUpperCase = wordComposer.isAllUpperCase();
- mTrailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
- mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions);
+ final ProximityInfo proximityInfo, final boolean isCorrectionEnabled) {
+ final int trailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
+ final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
+ MAX_SUGGESTIONS);
final String typedWord = wordComposer.getTypedWord();
- final String consideredWord = mTrailingSingleQuotesCount > 0
- ? typedWord.substring(0, typedWord.length() - mTrailingSingleQuotesCount)
+ final String consideredWord = trailingSingleQuotesCount > 0
+ ? typedWord.substring(0, typedWord.length() - trailingSingleQuotesCount)
: typedWord;
- // Treating USER_TYPED as UNIGRAM suggestion for logging now.
- LatinImeLogger.onAddSuggestedWord(typedWord, Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
- mConsideredWord = consideredWord;
-
- if (wordComposer.size() <= 1 && (correctionMode == CORRECTION_FULL_BIGRAM)) {
- // At first character typed, search only the bigrams
- mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS);
-
- if (!TextUtils.isEmpty(prevWordForBigram)) {
- getAllBigrams(prevWordForBigram, wordComposer);
- if (TextUtils.isEmpty(consideredWord)) {
- // Nothing entered: return all bigrams for the previous word
- int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
- for (int i = 0; i < insertCount; ++i) {
- addBigramToSuggestions(mBigramSuggestions.get(i));
- }
- } else {
- // Word entered: return only bigrams that match the first char of the typed word
- final char currentChar = consideredWord.charAt(0);
- // TODO: Must pay attention to locale when changing case.
- // TODO: Use codepoint instead of char
- final char currentCharUpper = Character.toUpperCase(currentChar);
- int count = 0;
- final int bigramSuggestionSize = mBigramSuggestions.size();
- for (int i = 0; i < bigramSuggestionSize; i++) {
- final SuggestedWordInfo bigramSuggestion = mBigramSuggestions.get(i);
- final char bigramSuggestionFirstChar =
- (char)bigramSuggestion.codePointAt(0);
- if (bigramSuggestionFirstChar == currentChar
- || bigramSuggestionFirstChar == currentCharUpper) {
- addBigramToSuggestions(bigramSuggestion);
- if (++count > mPrefMaxSuggestions) break;
- }
- }
- }
- }
+ LatinImeLogger.onAddSuggestedWord(typedWord, Dictionary.TYPE_USER_TYPED);
- } else if (wordComposer.size() > 1) {
- final WordComposer wordComposerForLookup;
- if (mTrailingSingleQuotesCount > 0) {
- wordComposerForLookup = new WordComposer(wordComposer);
- for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
- wordComposerForLookup.deleteLast();
- }
- } else {
- wordComposerForLookup = wordComposer;
- }
- // At second character typed, search the unigrams (scores being affected by bigrams)
- for (final String key : mUnigramDictionaries.keySet()) {
- // Skip UserUnigramDictionary and WhitelistDictionary to lookup
- if (key.equals(DICT_KEY_USER_HISTORY_UNIGRAM) || key.equals(DICT_KEY_WHITELIST))
- continue;
- final Dictionary dictionary = mUnigramDictionaries.get(key);
- dictionary.getWords(wordComposerForLookup, prevWordForBigram, this, proximityInfo);
+ final WordComposer wordComposerForLookup;
+ if (trailingSingleQuotesCount > 0) {
+ wordComposerForLookup = new WordComposer(wordComposer);
+ for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) {
+ wordComposerForLookup.deleteLast();
}
+ } else {
+ wordComposerForLookup = wordComposer;
}
- final CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase,
- mIsFirstCharCapitalized, mWhiteListDictionary.getWhitelistedWord(consideredWord));
+ for (final String key : mDictionaries.keySet()) {
+ final Dictionary dictionary = mDictionaries.get(key);
+ suggestionsSet.addAll(dictionary.getSuggestions(
+ wordComposerForLookup, prevWordForBigram, proximityInfo));
+ }
- final boolean hasAutoCorrection;
- if (CORRECTION_FULL == correctionMode || CORRECTION_FULL_BIGRAM == correctionMode) {
- final CharSequence autoCorrection =
- AutoCorrection.computeAutoCorrectionWord(mUnigramDictionaries, wordComposer,
- mSuggestions, consideredWord, mAutoCorrectionThreshold,
- whitelistedWord);
- hasAutoCorrection = (null != autoCorrection);
+ final CharSequence whitelistedWord;
+ if (suggestionsSet.isEmpty()) {
+ whitelistedWord = null;
+ } else if (SuggestedWordInfo.KIND_WHITELIST != suggestionsSet.first().mKind) {
+ whitelistedWord = null;
} else {
+ whitelistedWord = suggestionsSet.first().mWord;
+ }
+
+ // The word can be auto-corrected if it has a whitelist entry that is not itself,
+ // or if it's a 2+ characters non-word (i.e. it's not in the dictionary).
+ final boolean allowsToBeAutoCorrected = (null != whitelistedWord
+ && !whitelistedWord.equals(consideredWord))
+ || (consideredWord.length() > 1 && !AutoCorrection.isInTheDictionary(mDictionaries,
+ consideredWord, wordComposer.isFirstCharCapitalized()));
+
+ final boolean hasAutoCorrection;
+ // TODO: using isCorrectionEnabled here is not very good. It's probably useless, because
+ // any attempt to do auto-correction is already shielded with a test for this flag; at the
+ // same time, it feels wrong that the SuggestedWord object includes information about
+ // the current settings. It may also be useful to know, when the setting is off, whether
+ // the word *would* have been auto-corrected.
+ if (!isCorrectionEnabled || !allowsToBeAutoCorrected || !wordComposer.isComposingWord()
+ || suggestionsSet.isEmpty() || wordComposer.hasDigits()
+ || wordComposer.isMostlyCaps() || wordComposer.isResumed()
+ || !hasMainDictionary()) {
+ // If we don't have a main dictionary, we never want to auto-correct. The reason for
+ // this is, the user may have a contact whose name happens to match a valid word in
+ // their language, and it will unexpectedly auto-correct. For example, if the user
+ // types in English with no dictionary and has a "Will" in their contact list, "will"
+ // would always auto-correct to "Will" which is unwanted. Hence, no main dict => no
+ // auto-correct.
hasAutoCorrection = false;
+ } else {
+ hasAutoCorrection = AutoCorrection.suggestionExceedsAutoCorrectionThreshold(
+ suggestionsSet.first(), consideredWord, mAutoCorrectionThreshold);
}
- if (whitelistedWord != null) {
- if (mTrailingSingleQuotesCount > 0) {
- final StringBuilder sb = new StringBuilder(whitelistedWord);
- for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
- sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
- }
- mSuggestions.add(0, new SuggestedWordInfo(
- sb.toString(), SuggestedWordInfo.MAX_SCORE));
- } else {
- mSuggestions.add(0, new SuggestedWordInfo(
- whitelistedWord, SuggestedWordInfo.MAX_SCORE));
+ final ArrayList<SuggestedWordInfo> suggestionsContainer =
+ CollectionUtils.newArrayList(suggestionsSet);
+ final int suggestionsCount = suggestionsContainer.size();
+ final boolean isFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
+ final boolean isAllUpperCase = wordComposer.isAllUpperCase();
+ if (isFirstCharCapitalized || isAllUpperCase || 0 != trailingSingleQuotesCount) {
+ for (int i = 0; i < suggestionsCount; ++i) {
+ final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
+ final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
+ wordInfo, mLocale, isAllUpperCase, isFirstCharCapitalized,
+ trailingSingleQuotesCount);
+ suggestionsContainer.set(i, transformedWordInfo);
}
}
- mSuggestions.add(0, new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE));
- SuggestedWordInfo.removeDups(mSuggestions);
+ for (int i = 0; i < suggestionsCount; ++i) {
+ final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
+ LatinImeLogger.onAddSuggestedWord(wordInfo.mWord.toString(), wordInfo.mSourceDict);
+ }
+
+ if (!TextUtils.isEmpty(typedWord)) {
+ suggestionsContainer.add(0, new SuggestedWordInfo(typedWord,
+ SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED,
+ Dictionary.TYPE_USER_TYPED));
+ }
+ SuggestedWordInfo.removeDups(suggestionsContainer);
final ArrayList<SuggestedWordInfo> suggestionsList;
- if (DBG) {
- suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, mSuggestions);
+ if (DBG && !suggestionsContainer.isEmpty()) {
+ suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, suggestionsContainer);
} else {
- suggestionsList = mSuggestions;
+ suggestionsList = suggestionsContainer;
}
- // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid"
- // but still autocorrected from - in the case the whitelist only capitalizes the word.
- // The whitelist should be case-insensitive, so it's not possible to be consistent with
- // a boolean flag. Right now this is handled with a slight hack in
- // WhitelistDictionary#shouldForciblyAutoCorrectFrom.
- final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected(
- getUnigramDictionaries(), consideredWord, wordComposer.isFirstCharCapitalized())
- // If we don't have a main dictionary, we never want to auto-correct. The reason for this
- // is, the user may have a contact whose name happens to match a valid word in their
- // language, and it will unexpectedly auto-correct. For example, if the user types in
- // English with no dictionary and has a "Will" in their contact list, "will" would
- // always auto-correct to "Will" which is unwanted. Hence, no main dict => no auto-correct.
- && mHasMainDictionary;
-
- boolean autoCorrectionAvailable = hasAutoCorrection;
- if (correctionMode == CORRECTION_FULL || correctionMode == CORRECTION_FULL_BIGRAM) {
- autoCorrectionAvailable |= !allowsToBeAutoCorrected;
- }
- // Don't auto-correct words with multiple capital letter
- autoCorrectionAvailable &= !wordComposer.isMostlyCaps();
- autoCorrectionAvailable &= !wordComposer.isResumed();
- if (allowsToBeAutoCorrected && suggestionsList.size() > 1 && mAutoCorrectionThreshold > 0
- && Suggest.shouldBlockAutoCorrectionBySafetyNet(typedWord,
- suggestionsList.get(1).mWord)) {
- autoCorrectionAvailable = false;
- }
return 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.
!allowsToBeAutoCorrected /* typedWordValid */,
- autoCorrectionAvailable /* hasAutoCorrectionCandidate */,
- allowsToBeAutoCorrected /* allowsToBeAutoCorrected */,
+ hasAutoCorrection, /* willAutoCorrect */
false /* isPunctuationSuggestions */,
false /* isObsoleteSuggestions */,
- false /* isPrediction */);
+ !wordComposer.isComposingWord() /* isPrediction */);
}
- /**
- * Adds all bigram predictions for prevWord. Also checks the lower case version of prevWord if
- * it contains any upper case characters.
- */
- private void getAllBigrams(final CharSequence prevWord, final WordComposer wordComposer) {
- if (StringUtils.hasUpperCase(prevWord)) {
- // TODO: Must pay attention to locale when changing case.
- final CharSequence lowerPrevWord = prevWord.toString().toLowerCase();
- for (final Dictionary dictionary : mBigramDictionaries.values()) {
- dictionary.getBigrams(wordComposer, lowerPrevWord, this);
+ // Retrieves suggestions for the batch input.
+ private SuggestedWords getSuggestedWordsForBatchInput(
+ final WordComposer wordComposer, CharSequence prevWordForBigram,
+ final ProximityInfo proximityInfo, int sessionId) {
+ 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, sessionId));
}
- for (final Dictionary dictionary : mBigramDictionaries.values()) {
- dictionary.getBigrams(wordComposer, prevWord, this);
+
+ for (SuggestedWordInfo wordInfo : suggestionsSet) {
+ LatinImeLogger.onAddSuggestedWord(wordInfo.mWord.toString(), wordInfo.mSourceDict);
}
+
+ final ArrayList<SuggestedWordInfo> suggestionsContainer =
+ CollectionUtils.newArrayList(suggestionsSet);
+ final int suggestionsCount = suggestionsContainer.size();
+ final boolean isFirstCharCapitalized = wordComposer.wasShiftedNoLock();
+ final boolean isAllUpperCase = wordComposer.isAllUpperCase();
+ if (isFirstCharCapitalized || isAllUpperCase) {
+ for (int i = 0; i < suggestionsCount; ++i) {
+ final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
+ final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
+ wordInfo, mLocale, isAllUpperCase, isFirstCharCapitalized,
+ 0 /* trailingSingleQuotesCount */);
+ suggestionsContainer.set(i, transformedWordInfo);
+ }
+ }
+
+ SuggestedWordInfo.removeDups(suggestionsContainer);
+ // 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,
+ true /* typedWordValid */,
+ false /* willAutoCorrect */,
+ false /* isPunctuationSuggestions */,
+ false /* isObsoleteSuggestions */,
+ false /* isPrediction */);
}
private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
@@ -411,7 +342,7 @@ public class Suggest implements Dictionary.WordCallback {
typedWordInfo.setDebugString("+");
final int suggestionsSize = suggestions.size();
final ArrayList<SuggestedWordInfo> suggestionsList =
- new ArrayList<SuggestedWordInfo>(suggestionsSize);
+ CollectionUtils.newArrayList(suggestionsSize);
suggestionsList.add(typedWordInfo);
// Note: i here is the index in mScores[], but the index in mSuggestions is one more
// than i because we added the typed word to mSuggestions without touching mScores.
@@ -431,119 +362,45 @@ public class Suggest implements Dictionary.WordCallback {
return suggestionsList;
}
- // TODO: Use codepoint instead of char
- @Override
- public boolean addWord(final char[] word, final int offset, final int length, int score,
- final int dicTypeId, final int dataType) {
- int dataTypeForLog = dataType;
- final ArrayList<SuggestedWordInfo> suggestions;
- final int prefMaxSuggestions;
- if (dataType == Dictionary.BIGRAM) {
- suggestions = mBigramSuggestions;
- prefMaxSuggestions = PREF_MAX_BIGRAMS;
- } else {
- suggestions = mSuggestions;
- prefMaxSuggestions = mPrefMaxSuggestions;
+ private static final class SuggestedWordInfoComparator
+ implements Comparator<SuggestedWordInfo> {
+ // This comparator ranks the word info with the higher frequency first. That's because
+ // that's the order we want our elements in.
+ @Override
+ public int compare(final SuggestedWordInfo o1, final SuggestedWordInfo o2) {
+ if (o1.mScore > o2.mScore) return -1;
+ if (o1.mScore < o2.mScore) return 1;
+ if (o1.mCodePointCount < o2.mCodePointCount) return -1;
+ if (o1.mCodePointCount > o2.mCodePointCount) return 1;
+ return o1.mWord.toString().compareTo(o2.mWord.toString());
}
-
- int pos = 0;
-
- // Check if it's the same word, only caps are different
- if (StringUtils.equalsIgnoreCase(mConsideredWord, word, offset, length)) {
- // TODO: remove this surrounding if clause and move this logic to
- // getSuggestedWordBuilder.
- if (suggestions.size() > 0) {
- final SuggestedWordInfo currentHighestWord = suggestions.get(0);
- // If the current highest word is also equal to typed word, we need to compare
- // frequency to determine the insertion position. This does not ensure strictly
- // correct ordering, but ensures the top score is on top which is enough for
- // removing duplicates correctly.
- if (StringUtils.equalsIgnoreCase(currentHighestWord.mWord, word, offset, length)
- && score <= currentHighestWord.mScore) {
- pos = 1;
- }
- }
- } else {
- // Check the last one's score and bail
- if (suggestions.size() >= prefMaxSuggestions
- && suggestions.get(prefMaxSuggestions - 1).mScore >= score) return true;
- while (pos < suggestions.size()) {
- final int curScore = suggestions.get(pos).mScore;
- if (curScore < score
- || (curScore == score && length < suggestions.get(pos).codePointCount())) {
- break;
- }
- pos++;
- }
- }
- if (pos >= prefMaxSuggestions) {
- return true;
- }
-
- final StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
- // TODO: Must pay attention to locale when changing case.
- if (mIsAllUpperCase) {
- sb.append(new String(word, offset, length).toUpperCase());
- } else if (mIsFirstCharCapitalized) {
- sb.append(Character.toUpperCase(word[offset]));
- if (length > 1) {
- sb.append(word, offset + 1, length - 1);
- }
+ }
+ private static final SuggestedWordInfoComparator sSuggestedWordInfoComparator =
+ new SuggestedWordInfoComparator();
+
+ private 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());
+ if (isAllUpperCase) {
+ sb.append(wordInfo.mWord.toString().toUpperCase(locale));
+ } else if (isFirstCharCapitalized) {
+ sb.append(StringUtils.toTitleCase(wordInfo.mWord.toString(), locale));
} else {
- sb.append(word, offset, length);
+ sb.append(wordInfo.mWord);
}
- for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
+ for (int i = trailingSingleQuotesCount - 1; i >= 0; --i) {
sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
}
- suggestions.add(pos, new SuggestedWordInfo(sb, score));
- if (suggestions.size() > prefMaxSuggestions) {
- suggestions.remove(prefMaxSuggestions);
- } else {
- LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId, dataTypeForLog);
- }
- return true;
+ return new SuggestedWordInfo(sb, wordInfo.mScore, wordInfo.mKind, wordInfo.mSourceDict);
}
public void close() {
- final HashSet<Dictionary> dictionaries = new HashSet<Dictionary>();
- dictionaries.addAll(mUnigramDictionaries.values());
- dictionaries.addAll(mBigramDictionaries.values());
+ final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet();
+ dictionaries.addAll(mDictionaries.values());
for (final Dictionary dictionary : dictionaries) {
dictionary.close();
}
- mHasMainDictionary = false;
- }
-
- // TODO: Resolve the inconsistencies between the native auto correction algorithms and
- // this safety net
- public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord,
- final CharSequence suggestion) {
- // Safety net for auto correction.
- // Actually if we hit this safety net, it's a bug.
- // If user selected aggressive auto correction mode, there is no need to use the safety
- // net.
- // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
- // we should not use net because relatively edit distance can be big.
- final int typedWordLength = typedWord.length();
- if (typedWordLength < Suggest.MINIMUM_SAFETY_NET_CHAR_LENGTH) {
- return false;
- }
- final int maxEditDistanceOfNativeDictionary =
- (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1;
- final int distance = BinaryDictionary.editDistance(typedWord, suggestion.toString());
- if (DBG) {
- Log.d(TAG, "Autocorrected edit distance = " + distance
- + ", " + maxEditDistanceOfNativeDictionary);
- }
- if (distance > maxEditDistanceOfNativeDictionary) {
- if (DBG) {
- Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestion);
- Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. "
- + "Turning off auto-correction.");
- }
- return true;
- } else {
- return false;
- }
+ mMainDictionary = null;
}
}
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 497fd3bfa..52e292a86 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -23,29 +23,31 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
-public class SuggestedWords {
+public final class SuggestedWords {
+ private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST =
+ CollectionUtils.newArrayList(0);
public static final SuggestedWords EMPTY = new SuggestedWords(
- new ArrayList<SuggestedWordInfo>(0), false, false, false, false, false, false);
+ EMPTY_WORD_INFO_LIST, false, false, false, false, false);
public final boolean mTypedWordValid;
- public final boolean mHasAutoCorrectionCandidate;
+ // Note: this INCLUDES cases where the word will auto-correct to itself. A good definition
+ // of what this flag means would be "the top suggestion is strong enough to auto-correct",
+ // whether this exactly matches the user entry or not.
+ public final boolean mWillAutoCorrect;
public final boolean mIsPunctuationSuggestions;
- public final boolean mAllowsToBeAutoCorrected;
public final boolean mIsObsoleteSuggestions;
public final boolean mIsPrediction;
private final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList;
public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
final boolean typedWordValid,
- final boolean hasAutoCorrectionCandidate,
- final boolean allowsToBeAutoCorrected,
+ final boolean willAutoCorrect,
final boolean isPunctuationSuggestions,
final boolean isObsoleteSuggestions,
final boolean isPrediction) {
mSuggestedWordInfoList = suggestedWordInfoList;
mTypedWordValid = typedWordValid;
- mHasAutoCorrectionCandidate = hasAutoCorrectionCandidate;
- mAllowsToBeAutoCorrected = allowsToBeAutoCorrected;
+ mWillAutoCorrect = willAutoCorrect;
mIsPunctuationSuggestions = isPunctuationSuggestions;
mIsObsoleteSuggestions = isObsoleteSuggestions;
mIsPrediction = isPrediction;
@@ -55,7 +57,7 @@ public class SuggestedWords {
return mSuggestedWordInfoList.size();
}
- public CharSequence getWord(int pos) {
+ public String getWord(int pos) {
return mSuggestedWordInfoList.get(pos).mWord;
}
@@ -67,12 +69,8 @@ public class SuggestedWords {
return mSuggestedWordInfoList.get(pos);
}
- public boolean hasAutoCorrectionWord() {
- return mHasAutoCorrectionCandidate && size() > 1 && !mTypedWordValid;
- }
-
public boolean willAutoCorrect() {
- return !mTypedWordValid && mHasAutoCorrectionCandidate;
+ return mWillAutoCorrect;
}
@Override
@@ -80,18 +78,18 @@ public class SuggestedWords {
// Pretty-print method to help debug
return "SuggestedWords:"
+ " mTypedWordValid=" + mTypedWordValid
- + " mHasAutoCorrectionCandidate=" + mHasAutoCorrectionCandidate
- + " mAllowsToBeAutoCorrected=" + mAllowsToBeAutoCorrected
+ + " mWillAutoCorrect=" + mWillAutoCorrect
+ " mIsPunctuationSuggestions=" + mIsPunctuationSuggestions
+ " words=" + Arrays.toString(mSuggestedWordInfoList.toArray());
}
public static ArrayList<SuggestedWordInfo> getFromApplicationSpecifiedCompletions(
final CompletionInfo[] infos) {
- final ArrayList<SuggestedWordInfo> result = new ArrayList<SuggestedWordInfo>();
+ final ArrayList<SuggestedWordInfo> result = CollectionUtils.newArrayList();
for (CompletionInfo info : infos) {
if (null != info && info.getText() != null) {
- result.add(new SuggestedWordInfo(info.getText(), SuggestedWordInfo.MAX_SCORE));
+ result.add(new SuggestedWordInfo(info.getText(), SuggestedWordInfo.MAX_SCORE,
+ SuggestedWordInfo.KIND_APP_DEFINED, Dictionary.TYPE_APPLICATION_DEFINED));
}
}
return result;
@@ -101,9 +99,10 @@ public class SuggestedWords {
// and replace it with what the user currently typed.
public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions(
final CharSequence typedWord, final SuggestedWords previousSuggestions) {
- final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<SuggestedWordInfo>();
- final HashSet<String> alreadySeen = new HashSet<String>();
- suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE));
+ 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));
alreadySeen.add(typedWord.toString());
final int previousSize = previousSuggestions.size();
for (int pos = 1; pos < previousSize; pos++) {
@@ -118,19 +117,31 @@ public class SuggestedWords {
return suggestionsList;
}
- public static class SuggestedWordInfo {
+ public static final class SuggestedWordInfo {
public static final int MAX_SCORE = Integer.MAX_VALUE;
- private final String mWordStr;
- public final CharSequence mWord;
+ public static final int KIND_TYPED = 0; // What user typed
+ public static final int KIND_CORRECTION = 1; // Simple correction/suggestion
+ public static final int KIND_COMPLETION = 2; // Completion (suggestion with appended chars)
+ public static final int KIND_WHITELIST = 3; // Whitelisted word
+ public static final int KIND_BLACKLIST = 4; // Blacklisted word
+ public static final int KIND_HARDCODED = 5; // Hardcoded suggestion, e.g. punctuation
+ public static final int KIND_APP_DEFINED = 6; // Suggested by the application
+ public static final int KIND_SHORTCUT = 7; // A shortcut
+ public static final int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input)
+ public final String mWord;
public final int mScore;
+ public final int mKind; // one of the KIND_* constants above
public final int mCodePointCount;
+ public final String mSourceDict;
private String mDebugString = "";
- public SuggestedWordInfo(final CharSequence word, final int score) {
- mWordStr = word.toString();
- mWord = word;
+ public SuggestedWordInfo(final CharSequence word, final int score, final int kind,
+ final String sourceDict) {
+ mWord = word.toString();
mScore = score;
- mCodePointCount = mWordStr.codePointCount(0, mWordStr.length());
+ mKind = kind;
+ mSourceDict = sourceDict;
+ mCodePointCount = StringUtils.codePointCount(mWord);
}
@@ -148,15 +159,15 @@ public class SuggestedWords {
}
public int codePointAt(int i) {
- return mWordStr.codePointAt(i);
+ return mWord.codePointAt(i);
}
@Override
public String toString() {
if (TextUtils.isEmpty(mDebugString)) {
- return mWordStr;
+ return mWord;
} else {
- return mWordStr + " (" + mDebugString.toString() + ")";
+ return mWord + " (" + mDebugString.toString() + ")";
}
}
@@ -166,11 +177,11 @@ public class SuggestedWords {
return;
}
int i = 1;
- while(i < candidates.size()) {
+ while (i < candidates.size()) {
final SuggestedWordInfo cur = candidates.get(i);
for (int j = 0; j < i; ++j) {
final SuggestedWordInfo previous = candidates.get(j);
- if (TextUtils.equals(cur.mWord, previous.mWord)) {
+ if (cur.mWord.equals(previous.mWord)) {
candidates.remove(cur.mScore < previous.mScore ? i : j);
--i;
break;
diff --git a/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java b/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java
index 4a3f42d5d..d188fc5ef 100644
--- a/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java
+++ b/java/src/com/android/inputmethod/latin/SuggestionSpanPickedNotificationReceiver.java
@@ -23,7 +23,7 @@ import android.content.Context;
import android.content.Intent;
import android.util.Log;
-public class SuggestionSpanPickedNotificationReceiver extends BroadcastReceiver {
+public final class SuggestionSpanPickedNotificationReceiver extends BroadcastReceiver {
private static final boolean DBG = LatinImeLogger.sDBG;
private static final String TAG =
SuggestionSpanPickedNotificationReceiver.class.getSimpleName();
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
index 673b54500..8f21b7b4a 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsBinaryDictionary.java
@@ -19,22 +19,23 @@ package com.android.inputmethod.latin;
import android.content.Context;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import java.util.ArrayList;
import java.util.Locale;
-public class SynchronouslyLoadedContactsBinaryDictionary extends ContactsBinaryDictionary {
+public final class SynchronouslyLoadedContactsBinaryDictionary extends ContactsBinaryDictionary {
private boolean mClosed;
public SynchronouslyLoadedContactsBinaryDictionary(final Context context, final Locale locale) {
- super(context, Suggest.DIC_CONTACTS, locale);
+ super(context, locale);
}
@Override
- public synchronized void getWords(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
+ public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
+ final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) {
syncReloadDictionaryIfRequired();
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java
deleted file mode 100644
index a8b871cdf..000000000
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedContactsDictionary.java
+++ /dev/null
@@ -1,54 +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;
-
-import android.content.Context;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-public class SynchronouslyLoadedContactsDictionary extends ContactsDictionary {
- private boolean mClosed;
-
- public SynchronouslyLoadedContactsDictionary(final Context context) {
- super(context, Suggest.DIC_CONTACTS);
- mClosed = false;
- }
-
- @Override
- public synchronized void getWords(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
- blockingReloadDictionaryIfRequired();
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
- }
-
- @Override
- public synchronized boolean isValidWord(CharSequence word) {
- blockingReloadDictionaryIfRequired();
- return getWordFrequency(word) > -1;
- }
-
- // Protect against multiple closing
- @Override
- public synchronized void close() {
- // Actually with the current implementation of ContactsDictionary it's safe to close
- // several times, so the following protection is really only for foolproofing
- if (mClosed) return;
- mClosed = true;
- super.close();
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
index 1606a34e0..612f54d73 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserBinaryDictionary.java
@@ -19,8 +19,11 @@ package com.android.inputmethod.latin;
import android.content.Context;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-public class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionary {
+import java.util.ArrayList;
+
+public final class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionary {
public SynchronouslyLoadedUserBinaryDictionary(final Context context, final String locale) {
this(context, locale, false);
@@ -32,11 +35,10 @@ public class SynchronouslyLoadedUserBinaryDictionary extends UserBinaryDictionar
}
@Override
- public synchronized void getWords(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
+ public synchronized ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer codes,
+ final CharSequence prevWordForBigrams, final ProximityInfo proximityInfo) {
syncReloadDictionaryIfRequired();
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
+ return super.getSuggestions(codes, prevWordForBigrams, proximityInfo);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java
deleted file mode 100644
index 23a49c192..000000000
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java
+++ /dev/null
@@ -1,56 +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;
-
-import android.content.Context;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-public class SynchronouslyLoadedUserDictionary extends UserDictionary {
- private boolean mClosed;
-
- public SynchronouslyLoadedUserDictionary(final Context context, final String locale) {
- this(context, locale, false);
- }
-
- public SynchronouslyLoadedUserDictionary(final Context context, final String locale,
- final boolean alsoUseMoreRestrictiveLocales) {
- super(context, locale, alsoUseMoreRestrictiveLocales);
- }
-
- @Override
- public synchronized void getWords(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
- blockingReloadDictionaryIfRequired();
- getWordsInner(codes, prevWordForBigrams, callback, proximityInfo);
- }
-
- @Override
- public synchronized boolean isValidWord(CharSequence word) {
- blockingReloadDictionaryIfRequired();
- return super.isValidWord(word);
- }
-
- // Protect against multiple closing
- @Override
- public synchronized void close() {
- if (mClosed) return;
- mClosed = true;
- super.close();
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/TargetApplicationGetter.java b/java/src/com/android/inputmethod/latin/TargetApplicationGetter.java
index 4265309e5..743a8c60f 100644
--- a/java/src/com/android/inputmethod/latin/TargetApplicationGetter.java
+++ b/java/src/com/android/inputmethod/latin/TargetApplicationGetter.java
@@ -22,8 +22,7 @@ import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.util.LruCache;
-public class TargetApplicationGetter extends AsyncTask<String, Void, ApplicationInfo> {
-
+public final class TargetApplicationGetter extends AsyncTask<String, Void, ApplicationInfo> {
private static final int MAX_CACHE_ENTRIES = 64; // arbitrary
private static LruCache<String, ApplicationInfo> sCache =
new LruCache<String, ApplicationInfo>(MAX_CACHE_ENTRIES);
@@ -32,6 +31,7 @@ public class TargetApplicationGetter extends AsyncTask<String, Void, Application
if (null == packageName) return null;
return sCache.get(packageName);
}
+
public static void removeApplicationInfoCache(final String packageName) {
sCache.remove(packageName);
}
diff --git a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
index 6fa1a25a1..60e6fa127 100644
--- a/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserBinaryDictionary.java
@@ -34,13 +34,27 @@ import java.util.Arrays;
*/
public class UserBinaryDictionary extends ExpandableBinaryDictionary {
- // TODO: use Words.SHORTCUT when it's public in the SDK
+ // The user dictionary provider uses an empty string to mean "all languages".
+ private static final String USER_DICTIONARY_ALL_LANGUAGES = "";
+
+ // TODO: use Words.SHORTCUT when we target JellyBean or above
final static String SHORTCUT = "shortcut";
- private static final String[] PROJECTION_QUERY = {
- Words.WORD,
- SHORTCUT,
- Words.FREQUENCY,
- };
+ private static final String[] PROJECTION_QUERY;
+ static {
+ // 16 is JellyBean, but we want this to compile against ICS.
+ if (android.os.Build.VERSION.SDK_INT >= 16) {
+ PROJECTION_QUERY = new String[] {
+ Words.WORD,
+ SHORTCUT,
+ Words.FREQUENCY,
+ };
+ } else {
+ PROJECTION_QUERY = new String[] {
+ Words.WORD,
+ Words.FREQUENCY,
+ };
+ }
+ }
private static final String NAME = "userunigram";
@@ -58,9 +72,14 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
public UserBinaryDictionary(final Context context, final String locale,
final boolean alsoUseMoreRestrictiveLocales) {
- super(context, getFilenameWithLocale(NAME, locale), Suggest.DIC_USER);
+ super(context, getFilenameWithLocale(NAME, locale), Dictionary.TYPE_USER);
if (null == locale) throw new NullPointerException(); // Catch the error earlier
- mLocale = locale;
+ if (SubtypeLocale.NO_LANGUAGE.equals(locale)) {
+ // If we don't have a locale, insert into the "all locales" user dictionary.
+ mLocale = USER_DICTIONARY_ALL_LANGUAGES;
+ } else {
+ mLocale = locale;
+ }
mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
// Perform a managed query. The Activity will handle closing and re-querying the cursor
// when needed.
@@ -136,7 +155,7 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
requestArguments = localeElements;
}
final Cursor cursor = mContext.getContentResolver().query(
- Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null);
+ Words.CONTENT_URI, PROJECTION_QUERY, request.toString(), requestArguments, null);
try {
addWords(cursor);
} finally {
@@ -182,16 +201,18 @@ public class UserBinaryDictionary extends ExpandableBinaryDictionary {
}
private void addWords(Cursor cursor) {
+ // 16 is JellyBean, but we want this to compile against ICS.
+ final boolean hasShortcutColumn = android.os.Build.VERSION.SDK_INT >= 16;
clearFusionDictionary();
if (cursor == null) return;
if (cursor.moveToFirst()) {
final int indexWord = cursor.getColumnIndex(Words.WORD);
- final int indexShortcut = cursor.getColumnIndex(SHORTCUT);
+ final int indexShortcut = hasShortcutColumn ? cursor.getColumnIndex(SHORTCUT) : 0;
final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
while (!cursor.isAfterLast()) {
- String word = cursor.getString(indexWord);
- String shortcut = cursor.getString(indexShortcut);
- int frequency = cursor.getInt(indexFrequency);
+ final String word = cursor.getString(indexWord);
+ final String shortcut = hasShortcutColumn ? cursor.getString(indexShortcut) : null;
+ final int frequency = cursor.getInt(indexFrequency);
// Safeguard against adding really long words.
if (word.length() < MAX_WORD_LENGTH) {
super.addWord(word, null, frequency);
diff --git a/java/src/com/android/inputmethod/latin/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java
deleted file mode 100644
index c1efadd44..000000000
--- a/java/src/com/android/inputmethod/latin/UserDictionary.java
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright (C) 2008 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.ContentProviderClient;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.provider.UserDictionary.Words;
-import android.text.TextUtils;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-import java.util.Arrays;
-
-// TODO: This class is superseded by {@link UserBinaryDictionary}. Should be cleaned up.
-/**
- * An expandable dictionary that stores the words in the user unigram dictionary.
- *
- * @deprecated Use {@link UserBinaryDictionary}.
- */
-@Deprecated
-public class UserDictionary extends ExpandableDictionary {
-
- // TODO: use Words.SHORTCUT when it's public in the SDK
- final static String SHORTCUT = "shortcut";
- private static final String[] PROJECTION_QUERY = {
- Words.WORD,
- SHORTCUT,
- Words.FREQUENCY,
- };
-
- // This is not exported by the framework so we pretty much have to write it here verbatim
- private static final String ACTION_USER_DICTIONARY_INSERT =
- "com.android.settings.USER_DICTIONARY_INSERT";
-
- private ContentObserver mObserver;
- final private String mLocale;
- final private boolean mAlsoUseMoreRestrictiveLocales;
-
- public UserDictionary(final Context context, final String locale) {
- this(context, locale, false);
- }
-
- public UserDictionary(final Context context, final String locale,
- final boolean alsoUseMoreRestrictiveLocales) {
- super(context, Suggest.DIC_USER);
- if (null == locale) throw new NullPointerException(); // Catch the error earlier
- mLocale = locale;
- mAlsoUseMoreRestrictiveLocales = alsoUseMoreRestrictiveLocales;
- // Perform a managed query. The Activity will handle closing and re-querying the cursor
- // when needed.
- ContentResolver cres = context.getContentResolver();
-
- mObserver = new ContentObserver(null) {
- @Override
- public void onChange(boolean self) {
- setRequiresReload(true);
- }
- };
- cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
-
- loadDictionary();
- }
-
- @Override
- public synchronized void close() {
- if (mObserver != null) {
- getContext().getContentResolver().unregisterContentObserver(mObserver);
- mObserver = null;
- }
- super.close();
- }
-
- @Override
- public void loadDictionaryAsync() {
- // Split the locale. For example "en" => ["en"], "de_DE" => ["de", "DE"],
- // "en_US_foo_bar_qux" => ["en", "US", "foo_bar_qux"] because of the limit of 3.
- // This is correct for locale processing.
- // For this example, we'll look at the "en_US_POSIX" case.
- final String[] localeElements =
- TextUtils.isEmpty(mLocale) ? new String[] {} : mLocale.split("_", 3);
- final int length = localeElements.length;
-
- final StringBuilder request = new StringBuilder("(locale is NULL)");
- String localeSoFar = "";
- // At start, localeElements = ["en", "US", "POSIX"] ; localeSoFar = "" ;
- // and request = "(locale is NULL)"
- for (int i = 0; i < length; ++i) {
- // i | localeSoFar | localeElements
- // 0 | "" | ["en", "US", "POSIX"]
- // 1 | "en_" | ["en", "US", "POSIX"]
- // 2 | "en_US_" | ["en", "en_US", "POSIX"]
- localeElements[i] = localeSoFar + localeElements[i];
- localeSoFar = localeElements[i] + "_";
- // i | request
- // 0 | "(locale is NULL)"
- // 1 | "(locale is NULL) or (locale=?)"
- // 2 | "(locale is NULL) or (locale=?) or (locale=?)"
- request.append(" or (locale=?)");
- }
- // At the end, localeElements = ["en", "en_US", "en_US_POSIX"]; localeSoFar = en_US_POSIX_"
- // and request = "(locale is NULL) or (locale=?) or (locale=?) or (locale=?)"
-
- final String[] requestArguments;
- // If length == 3, we already have all the arguments we need (common prefix is meaningless
- // inside variants
- if (mAlsoUseMoreRestrictiveLocales && length < 3) {
- request.append(" or (locale like ?)");
- // The following creates an array with one more (null) position
- final String[] localeElementsWithMoreRestrictiveLocalesIncluded =
- Arrays.copyOf(localeElements, length + 1);
- localeElementsWithMoreRestrictiveLocalesIncluded[length] =
- localeElements[length - 1] + "_%";
- requestArguments = localeElementsWithMoreRestrictiveLocalesIncluded;
- // If for example localeElements = ["en"]
- // then requestArguments = ["en", "en_%"]
- // and request = (locale is NULL) or (locale=?) or (locale like ?)
- // If localeElements = ["en", "en_US"]
- // then requestArguments = ["en", "en_US", "en_US_%"]
- } else {
- requestArguments = localeElements;
- }
- final Cursor cursor = getContext().getContentResolver()
- .query(Words.CONTENT_URI, PROJECTION_QUERY, request.toString(),
- requestArguments, null);
- try {
- addWords(cursor);
- } finally {
- if (null != cursor) cursor.close();
- }
- }
-
- public boolean isEnabled() {
- final ContentResolver cr = getContext().getContentResolver();
- final ContentProviderClient client = cr.acquireContentProviderClient(Words.CONTENT_URI);
- if (client != null) {
- client.release();
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Adds a word to the user dictionary and makes it persistent.
- *
- * This will call upon the system interface to do the actual work through the intent
- * readied by the system to this effect.
- *
- * @param word the word to add. If the word is capitalized, then the dictionary will
- * recognize it as a capitalized word when searched.
- * @param frequency the frequency of occurrence of the word. A frequency of 255 is considered
- * the highest.
- * @TODO use a higher or float range for frequency
- */
- public synchronized void addWordToUserDictionary(final String word, final int frequency) {
- // Force load the dictionary here synchronously
- if (getRequiresReload()) loadDictionaryAsync();
- // TODO: do something for the UI. With the following, any sufficiently long word will
- // look like it will go to the user dictionary but it won't.
- // Safeguard against adding long words. Can cause stack overflow.
- if (word.length() >= getMaxWordLength()) return;
-
- // TODO: Add an argument to the intent to specify the frequency.
- Intent intent = new Intent(ACTION_USER_DICTIONARY_INSERT);
- intent.putExtra(Words.WORD, word);
- intent.putExtra(Words.LOCALE, mLocale);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- getContext().startActivity(intent);
- }
-
- @Override
- public synchronized void getWords(final WordComposer codes,
- final CharSequence prevWordForBigrams, final WordCallback callback,
- final ProximityInfo proximityInfo) {
- super.getWords(codes, prevWordForBigrams, callback, proximityInfo);
- }
-
- @Override
- public synchronized boolean isValidWord(CharSequence word) {
- return super.isValidWord(word);
- }
-
- private void addWords(Cursor cursor) {
- clearDictionary();
- if (cursor == null) return;
- final int maxWordLength = getMaxWordLength();
- if (cursor.moveToFirst()) {
- final int indexWord = cursor.getColumnIndex(Words.WORD);
- final int indexShortcut = cursor.getColumnIndex(SHORTCUT);
- final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
- while (!cursor.isAfterLast()) {
- String word = cursor.getString(indexWord);
- String shortcut = cursor.getString(indexShortcut);
- int frequency = cursor.getInt(indexFrequency);
- // Safeguard against adding really long words. Stack may overflow due
- // to recursion
- if (word.length() < maxWordLength) {
- super.addWord(word, null, frequency);
- }
- if (null != shortcut && shortcut.length() < maxWordLength) {
- super.addWord(shortcut, word, frequency);
- }
- cursor.moveToNext();
- }
- }
- }
-}
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java
new file mode 100644
index 000000000..e39011145
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictIOUtils.java
@@ -0,0 +1,214 @@
+/*
+ * 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.util.Log;
+
+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.FormatSpec.FormatOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.PendingAttribute;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Reads and writes Binary files for a UserHistoryDictionary.
+ *
+ * All the methods in this class are static.
+ */
+public final class UserHistoryDictIOUtils {
+ private static final String TAG = UserHistoryDictIOUtils.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ public interface OnAddWordListener {
+ public void setUnigram(final String word, final String shortcutTarget, final int frequency);
+ public void setBigram(final String word1, final String word2, final int frequency);
+ }
+
+ public interface BigramDictionaryInterface {
+ 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 ((int)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,
+ final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams,
+ final FormatOptions formatOptions) {
+ final FusionDictionary fusionDict = constructFusionDictionary(dict, bigrams);
+ try {
+ BinaryDictInputOutput.writeDictionaryBinary(destination, fusionDict, formatOptions);
+ Log.d(TAG, "end writing");
+ } catch (IOException e) {
+ Log.e(TAG, "IO exception while writing file: " + e);
+ } catch (UnsupportedFormatException e) {
+ Log.e(TAG, "Unsupported fomat: " + e);
+ }
+ }
+
+ /**
+ * Constructs a new FusionDictionary from BigramDictionaryInterface.
+ */
+ /* packages for test */ static FusionDictionary constructFusionDictionary(
+ final BigramDictionaryInterface dict, final UserHistoryDictionaryBigramList bigrams) {
+ final FusionDictionary fusionDict = new FusionDictionary(new Node(),
+ new FusionDictionary.DictionaryOptions(new HashMap<String, String>(), false,
+ false));
+ int profTotal = 0;
+ for (final String word1 : bigrams.keySet()) {
+ final HashMap<String, Byte> word1Bigrams = bigrams.getBigrams(word1);
+ for (final String word2 : word1Bigrams.keySet()) {
+ final int freq = dict.getFrequency(word1, word2);
+ if (freq == -1) {
+ // don't add this bigram.
+ continue;
+ }
+ if (DEBUG) {
+ if (word1 == null) {
+ Log.d(TAG, "add unigram: " + word2 + "," + Integer.toString(freq));
+ } else {
+ Log.d(TAG, "add bigram: " + word1
+ + "," + word2 + "," + Integer.toString(freq));
+ }
+ profTotal++;
+ }
+ if (word1 == null) { // unigram
+ fusionDict.add(word2, freq, null, false /* isNotAWord */);
+ } else { // bigram
+ if (FusionDictionary.findWordInTree(fusionDict.mRoot, word1) == null) {
+ fusionDict.add(word1, 2, null, false /* isNotAWord */);
+ }
+ fusionDict.setBigram(word1, word2, freq);
+ }
+ bigrams.updateBigram(word1, word2, (byte)freq);
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "add " + profTotal + "words");
+ }
+ return fusionDict;
+ }
+
+ /**
+ * Reads dictionary from file.
+ */
+ public static void readDictionaryBinary(final FusionDictionaryBufferInterface buffer,
+ 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();
+ try {
+ BinaryDictIOUtils.readUnigramsAndBigramsBinary(buffer, unigrams, frequencies,
+ bigrams);
+ } catch (IOException e) {
+ Log.e(TAG, "IO exception while reading file: " + e);
+ } catch (UnsupportedFormatException e) {
+ Log.e(TAG, "Unsupported format: " + e);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ Log.e(TAG, "ArrayIndexOutOfBoundsException while reading file: " + e);
+ }
+ addWordsFromWordMap(unigrams, frequencies, bigrams, dict);
+ }
+
+ /**
+ * Adds all unigrams and bigrams in maps to OnAddWordListener.
+ */
+ /* package for test */ 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()) {
+ final String word1 = entry.getValue();
+ final int unigramFrequency = frequencies.get(entry.getKey());
+ to.setUnigram(word1, null, unigramFrequency);
+ final ArrayList<PendingAttribute> attrList = bigrams.get(entry.getKey());
+ if (attrList != null) {
+ for (final PendingAttribute attr : attrList) {
+ to.setBigram(word1, unigrams.get(attr.mAddress),
+ BinaryDictInputOutput.reconstructBigramFrequency(unigramFrequency,
+ attr.mFrequency));
+ }
+ }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
index 5095f6582..3615fa1fb 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
@@ -16,21 +16,25 @@
package com.android.inputmethod.latin;
-import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.database.sqlite.SQLiteQueryBuilder;
import android.os.AsyncTask;
-import android.provider.BaseColumns;
import android.util.Log;
+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.HashMap;
+import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
@@ -38,47 +42,29 @@ 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 class UserHistoryDictionary extends ExpandableDictionary {
- private static final String TAG = "UserHistoryDictionary";
+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. */
- private static int sMaxHistoryBigrams = 10000;
+ 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.
*/
- private static int sDeleteHistoryBigrams = 1000;
-
- /**
- * Database version should increase if the database structure changes
- */
- private static final int DATABASE_VERSION = 1;
-
- private static final String DATABASE_NAME = "userbigram_dict.db";
-
- /** Name of the words table in the database */
- private static final String MAIN_TABLE_NAME = "main";
- // TODO: Consume less space by using a unique id for locale instead of the whole
- // 2-5 character string.
- private static final String MAIN_COLUMN_ID = BaseColumns._ID;
- private static final String MAIN_COLUMN_WORD1 = "word1";
- private static final String MAIN_COLUMN_WORD2 = "word2";
- private static final String MAIN_COLUMN_LOCALE = "locale";
-
- /** Name of the frequency table in the database */
- private static final String FREQ_TABLE_NAME = "frequency";
- private static final String FREQ_COLUMN_ID = BaseColumns._ID;
- private static final String FREQ_COLUMN_PAIR_ID = "pair_id";
- private static final String COLUMN_FORGETTING_CURVE_VALUE = "freq";
+ public static final int DELETE_HISTORY_BIGRAMS = 1000;
/** Locale for which this user history dictionary is storing words */
private final String mLocale;
@@ -88,35 +74,14 @@ public class UserHistoryDictionary extends ExpandableDictionary {
private final ReentrantLock mBigramListLock = new ReentrantLock();
private final SharedPreferences mPrefs;
- private final static HashMap<String, String> sDictProjectionMap;
- private final static ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>
- sLangDictCache = new ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>();
-
- static {
- sDictProjectionMap = new HashMap<String, String>();
- sDictProjectionMap.put(MAIN_COLUMN_ID, MAIN_COLUMN_ID);
- sDictProjectionMap.put(MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD1);
- sDictProjectionMap.put(MAIN_COLUMN_WORD2, MAIN_COLUMN_WORD2);
- sDictProjectionMap.put(MAIN_COLUMN_LOCALE, MAIN_COLUMN_LOCALE);
+ // Should always be false except when we use this class for test
+ /* package for test */ boolean isTest = false;
- sDictProjectionMap.put(FREQ_COLUMN_ID, FREQ_COLUMN_ID);
- sDictProjectionMap.put(FREQ_COLUMN_PAIR_ID, FREQ_COLUMN_PAIR_ID);
- sDictProjectionMap.put(COLUMN_FORGETTING_CURVE_VALUE, COLUMN_FORGETTING_CURVE_VALUE);
- }
+ private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>
+ sLangDictCache = CollectionUtils.newConcurrentHashMap();
- private static DatabaseHelper sOpenHelper = null;
-
- public void setDatabaseMax(int maxHistoryBigram) {
- sMaxHistoryBigrams = maxHistoryBigram;
- }
-
- public void setDatabaseDelete(int deleteHistoryBigram) {
- sDeleteHistoryBigrams = deleteHistoryBigram;
- }
-
- public synchronized static UserHistoryDictionary getInstance(
- final Context context, final String locale,
- final int dictTypeId, final SharedPreferences sp) {
+ 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();
@@ -128,19 +93,16 @@ public class UserHistoryDictionary extends ExpandableDictionary {
}
}
final UserHistoryDictionary dict =
- new UserHistoryDictionary(context, locale, dictTypeId, sp);
+ new UserHistoryDictionary(context, locale, sp);
sLangDictCache.put(locale, new SoftReference<UserHistoryDictionary>(dict));
return dict;
}
- private UserHistoryDictionary(final Context context, final String locale, final int dicTypeId,
- SharedPreferences sp) {
- super(context, dicTypeId);
+ private UserHistoryDictionary(final Context context, final String locale,
+ final SharedPreferences sp) {
+ super(context, Dictionary.TYPE_USER_HISTORY);
mLocale = locale;
mPrefs = sp;
- if (sOpenHelper == null) {
- sOpenHelper = new DatabaseHelper(getContext());
- }
if (mLocale != null && mLocale.length() > 1) {
loadDictionary();
}
@@ -158,6 +120,14 @@ public class UserHistoryDictionary extends ExpandableDictionary {
// super.close();
}
+ @Override
+ protected ArrayList<SuggestedWordInfo> getWordsInner(final WordComposer composer,
+ final CharSequence 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.
*/
@@ -176,10 +146,15 @@ public class UserHistoryDictionary extends ExpandableDictionary {
* The second word may not be null (a NullPointerException would be thrown).
*/
public int addToUserHistory(final String word1, String word2, boolean isValid) {
+ if (word2.length() >= BinaryDictionary.MAX_WORD_LENGTH ||
+ (word1 != null && word1.length() >= BinaryDictionary.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;
@@ -217,11 +192,8 @@ public class UserHistoryDictionary extends ExpandableDictionary {
* Schedules a background thread to write any pending words to the database.
*/
private void flushPendingWrites() {
- if (mBigramListLock.isLocked()) {
- return;
- }
// Create a background thread to write the pending entries
- new UpdateDbTask(sOpenHelper, mBigramList, mLocale, this, mPrefs).execute();
+ new UpdateBinaryTask(mBigramList, mLocale, this, mPrefs, getContext()).execute();
}
@Override
@@ -235,6 +207,8 @@ public class UserHistoryDictionary extends ExpandableDictionary {
}
}
+ private int profTotal;
+
private void loadDictionaryAsyncLocked() {
if (DBG_STRESS_TEST) {
try {
@@ -247,340 +221,181 @@ public class UserHistoryDictionary extends ExpandableDictionary {
final long last = SettingsValues.getLastUserHistoryWriteTime(mPrefs, mLocale);
final boolean initializing = last == 0;
final long now = System.currentTimeMillis();
- // Load the words that correspond to the current input locale
- final Cursor cursor = query(MAIN_COLUMN_LOCALE + "=?", new String[] { mLocale });
- if (null == cursor) return;
- try {
- // TODO: Call SQLiteDataBase.beginTransaction / SQLiteDataBase.endTransaction
- if (cursor.moveToFirst()) {
- final int word1Index = cursor.getColumnIndex(MAIN_COLUMN_WORD1);
- final int word2Index = cursor.getColumnIndex(MAIN_COLUMN_WORD2);
- final int fcIndex = cursor.getColumnIndex(COLUMN_FORGETTING_CURVE_VALUE);
- while (!cursor.isAfterLast()) {
- final String word1 = cursor.getString(word1Index);
- final String word2 = cursor.getString(word2Index);
- final int fc = cursor.getInt(fcIndex);
+ profTotal = 0;
+ final String fileName = NAME + "." + mLocale + ".dict";
+ final ExpandableDictionary dictionary = this;
+ final OnAddWordListener listener = new OnAddWordListener() {
+ @Override
+ public void setUnigram(String word, String shortcutTarget, 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(String word1, String word2, int frequency) {
+ if (word1.length() < BinaryDictionary.MAX_WORD_LENGTH
+ && word2.length() < BinaryDictionary.MAX_WORD_LENGTH) {
+ profTotal++;
if (DBG_SAVE_RESTORE) {
- Log.d(TAG, "--- Load user history: " + word1 + ", " + word2 + ","
- + mLocale + "," + this);
- }
- // Safeguard against adding really long words. Stack may overflow due
- // to recursive lookup
- if (null == word1) {
- super.addWord(word2, null /* shortcut */, fc);
- } else if (word1.length() < BinaryDictionary.MAX_WORD_LENGTH
- && word2.length() < BinaryDictionary.MAX_WORD_LENGTH) {
- super.setBigramAndGetFrequency(
- word1, word2, initializing ? new ForgettingCurveParams(true)
- : new ForgettingCurveParams(fc, now, last));
+ Log.d(TAG, "load bigram: " + word1 + "," + word2 + "," + frequency);
}
- mBigramList.addBigram(word1, word2, (byte)fc);
- cursor.moveToNext();
+ 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) {
+ Log.e(TAG, "when loading: file not found" + e);
+ } catch (IOException e) {
+ Log.e(TAG, "IOException when open bytebuffer: " + e);
} finally {
- cursor.close();
+ if (inStream != null) {
+ try {
+ inStream.close();
+ } catch (IOException e) {
+ // do nothing
+ }
+ }
if (PROFILE_SAVE_RESTORE) {
final long diff = System.currentTimeMillis() - now;
- Log.w(TAG, "PROF: Load User HistoryDictionary: "
- + mLocale + ", " + diff + "ms.");
+ Log.d(TAG, "PROF: Load UserHistoryDictionary: "
+ + mLocale + ", " + diff + "ms. load " + profTotal + "entries.");
}
}
}
/**
- * Query the database
- */
- private static Cursor query(String selection, String[] selectionArgs) {
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
-
- // main INNER JOIN frequency ON (main._id=freq.pair_id)
- qb.setTables(MAIN_TABLE_NAME + " INNER JOIN " + FREQ_TABLE_NAME + " ON ("
- + MAIN_TABLE_NAME + "." + MAIN_COLUMN_ID + "=" + FREQ_TABLE_NAME + "."
- + FREQ_COLUMN_PAIR_ID +")");
-
- qb.setProjectionMap(sDictProjectionMap);
-
- // Get the database and run the query
- try {
- SQLiteDatabase db = sOpenHelper.getReadableDatabase();
- Cursor c = qb.query(db,
- new String[] {
- MAIN_COLUMN_WORD1, MAIN_COLUMN_WORD2, COLUMN_FORGETTING_CURVE_VALUE },
- selection, selectionArgs, null, null, null);
- return c;
- } catch (android.database.sqlite.SQLiteCantOpenDatabaseException e) {
- // Can't open the database : presumably we can't access storage. That may happen
- // when the device is wedged; do a best effort to still start the keyboard.
- return null;
- }
- }
-
- /**
- * This class helps open, create, and upgrade the database file.
+ * Async task to write pending words to the binarydicts.
*/
- private static class DatabaseHelper extends SQLiteOpenHelper {
-
- DatabaseHelper(Context context) {
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- db.execSQL("PRAGMA foreign_keys = ON;");
- db.execSQL("CREATE TABLE " + MAIN_TABLE_NAME + " ("
- + MAIN_COLUMN_ID + " INTEGER PRIMARY KEY,"
- + MAIN_COLUMN_WORD1 + " TEXT,"
- + MAIN_COLUMN_WORD2 + " TEXT,"
- + MAIN_COLUMN_LOCALE + " TEXT"
- + ");");
- db.execSQL("CREATE TABLE " + FREQ_TABLE_NAME + " ("
- + FREQ_COLUMN_ID + " INTEGER PRIMARY KEY,"
- + FREQ_COLUMN_PAIR_ID + " INTEGER,"
- + COLUMN_FORGETTING_CURVE_VALUE + " INTEGER,"
- + "FOREIGN KEY(" + FREQ_COLUMN_PAIR_ID + ") REFERENCES " + MAIN_TABLE_NAME
- + "(" + MAIN_COLUMN_ID + ")" + " ON DELETE CASCADE"
- + ");");
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
- + newVersion + ", which will destroy all old data");
- db.execSQL("DROP TABLE IF EXISTS " + MAIN_TABLE_NAME);
- db.execSQL("DROP TABLE IF EXISTS " + FREQ_TABLE_NAME);
- onCreate(db);
- }
- }
-
- /**
- * Async task to write pending words to the database so that it stays in sync with
- * the in-memory trie.
- */
- private static class UpdateDbTask extends AsyncTask<Void, Void, Void> {
+ private static final class UpdateBinaryTask extends AsyncTask<Void, Void, Void>
+ implements BigramDictionaryInterface {
private final UserHistoryDictionaryBigramList mBigramList;
- private final DatabaseHelper mDbHelper;
+ private final boolean mAddLevel0Bigrams;
private final String mLocale;
private final UserHistoryDictionary mUserHistoryDictionary;
private final SharedPreferences mPrefs;
+ private final Context mContext;
- public UpdateDbTask(
- DatabaseHelper openHelper, UserHistoryDictionaryBigramList pendingWrites,
- String locale, UserHistoryDictionary dict, SharedPreferences prefs) {
+ public UpdateBinaryTask(UserHistoryDictionaryBigramList pendingWrites, String locale,
+ UserHistoryDictionary dict, SharedPreferences prefs, Context context) {
mBigramList = pendingWrites;
mLocale = locale;
- mDbHelper = openHelper;
mUserHistoryDictionary = dict;
mPrefs = prefs;
- }
-
- /** Prune any old data if the database is getting too big. */
- private static void checkPruneData(SQLiteDatabase db) {
- db.execSQL("PRAGMA foreign_keys = ON;");
- Cursor c = db.query(FREQ_TABLE_NAME, new String[] { FREQ_COLUMN_PAIR_ID },
- null, null, null, null, null);
- try {
- int totalRowCount = c.getCount();
- // prune out old data if we have too much data
- if (totalRowCount > sMaxHistoryBigrams) {
- int numDeleteRows = (totalRowCount - sMaxHistoryBigrams)
- + sDeleteHistoryBigrams;
- int pairIdColumnId = c.getColumnIndex(FREQ_COLUMN_PAIR_ID);
- c.moveToFirst();
- int count = 0;
- while (count < numDeleteRows && !c.isAfterLast()) {
- String pairId = c.getString(pairIdColumnId);
- // Deleting from MAIN table will delete the frequencies
- // due to FOREIGN KEY .. ON DELETE CASCADE
- db.delete(MAIN_TABLE_NAME, MAIN_COLUMN_ID + "=?",
- new String[] { pairId });
- c.moveToNext();
- count++;
- }
- }
- } finally {
- c.close();
- }
+ mContext = context;
+ mAddLevel0Bigrams = mBigramList.size() <= MAX_HISTORY_BIGRAMS;
}
@Override
protected Void doInBackground(Void... v) {
- SQLiteDatabase db = null;
- if (mUserHistoryDictionary.mBigramListLock.tryLock()) {
+ if (mUserHistoryDictionary.isTest) {
+ // If isTest == true, wait until the lock is released.
+ mUserHistoryDictionary.mBigramListLock.lock();
try {
- try {
- db = mDbHelper.getWritableDatabase();
- } catch (android.database.sqlite.SQLiteCantOpenDatabaseException e) {
- // If we can't open the db, don't do anything. Exit through the next test
- // for non-nullity of the db variable.
- }
- if (null == db) {
- // Not much we can do. Just exit.
- return null;
- }
- db.beginTransaction();
- return doLoadTaskLocked(db);
+ doWriteTaskLocked();
} finally {
- if (db != null) {
- db.endTransaction();
- }
mUserHistoryDictionary.mBigramListLock.unlock();
}
+ } else if (mUserHistoryDictionary.mBigramListLock.tryLock()) {
+ doWriteTaskLocked();
}
return null;
}
- private Void doLoadTaskLocked(SQLiteDatabase db) {
+ 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;
- int profTotal = 0;
- int profInsert = 0;
- int profDelete = 0;
- db.execSQL("PRAGMA foreign_keys = ON;");
- final boolean addLevel0Bigram = mBigramList.size() <= sMaxHistoryBigrams;
-
- // Write all the entries to the db
- for (String word1 : mBigramList.keySet()) {
- final HashMap<String, Byte> word1Bigrams = mBigramList.getBigrams(word1);
- for (String word2 : word1Bigrams.keySet()) {
- if (PROFILE_SAVE_RESTORE) {
- ++profTotal;
- }
- // Get new frequency. Do not insert unigrams/bigrams which freq is "-1".
- final int freq; // -1, or 0~255
- if (word1 == null) { // unigram
- freq = FREQUENCY_FOR_TYPED;
- final byte prevFc = word1Bigrams.get(word2);
- if (prevFc == FREQUENCY_FOR_TYPED) {
- // No need to update since we found no changes for this entry.
- // Just skip to the next entry.
- if (DBG_SAVE_RESTORE) {
- Log.d(TAG, "Skip update user history: " + word1 + "," + word2
- + "," + prevFc);
- }
- if (!DBG_ALWAYS_WRITE) {
- continue;
- }
- }
- } else { // bigram
- final NextWord nw = mUserHistoryDictionary.getBigramWord(word1, word2);
- if (nw != null) {
- final ForgettingCurveParams fcp = nw.getFcParams();
- final byte prevFc = word1Bigrams.get(word2);
- final byte fc = (byte)fcp.getFc();
- final boolean isValid = fcp.isValid();
- if (prevFc > 0 && prevFc == fc) {
- // No need to update since we found no changes for this entry.
- // Just skip to the next entry.
- if (DBG_SAVE_RESTORE) {
- Log.d(TAG, "Skip update user history: " + word1 + ","
- + word2 + "," + prevFc);
- }
- if (!DBG_ALWAYS_WRITE) {
- continue;
- } else {
- freq = fc;
- }
- } else if (UserHistoryForgettingCurveUtils.
- needsToSave(fc, isValid, addLevel0Bigram)) {
- freq = fc;
- } else {
- freq = -1;
- }
- } else {
- freq = -1;
- }
- }
- // TODO: this process of making a text search for each pair each time
- // is terribly inefficient. Optimize this.
- // Find pair id
- Cursor c = null;
+ 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 {
- if (null != word1) {
- c = db.query(MAIN_TABLE_NAME, new String[] { MAIN_COLUMN_ID },
- MAIN_COLUMN_WORD1 + "=? AND " + MAIN_COLUMN_WORD2 + "=? AND "
- + MAIN_COLUMN_LOCALE + "=?",
- new String[] { word1, word2, mLocale }, null, null,
- null);
- } else {
- c = db.query(MAIN_TABLE_NAME, new String[] { MAIN_COLUMN_ID },
- MAIN_COLUMN_WORD1 + " IS NULL AND " + MAIN_COLUMN_WORD2
- + "=? AND " + MAIN_COLUMN_LOCALE + "=?",
- new String[] { word2, mLocale }, null, null, null);
- }
-
- final int pairId;
- if (c.moveToFirst()) {
- if (PROFILE_SAVE_RESTORE) {
- ++profDelete;
- }
- // Delete existing pair
- pairId = c.getInt(c.getColumnIndex(MAIN_COLUMN_ID));
- db.delete(FREQ_TABLE_NAME, FREQ_COLUMN_PAIR_ID + "=?",
- new String[] { Integer.toString(pairId) });
- } else {
- // Create new pair
- Long pairIdLong = db.insert(MAIN_TABLE_NAME, null,
- getContentValues(word1, word2, mLocale));
- pairId = pairIdLong.intValue();
- }
- if (freq > 0) {
- if (PROFILE_SAVE_RESTORE) {
- ++profInsert;
- }
- if (DBG_SAVE_RESTORE) {
- Log.d(TAG, "--- Save user history: " + word1 + ", " + word2
- + mLocale + "," + this);
- }
- // Insert new frequency
- db.insert(FREQ_TABLE_NAME, null,
- getFrequencyContentValues(pairId, freq));
- // Update an existing bigram entry in mBigramList too in order to
- // synchronize the SQL DB and mBigramList.
- mBigramList.updateBigram(word1, word2, (byte)freq);
- }
- } finally {
- if (c != null) {
- c.close();
- }
+ out.close();
+ } catch (IOException e) {
+ // ignore
}
}
}
- checkPruneData(db);
- // Save the timestamp after we finish writing the SQL DB.
+ // Save the timestamp after we finish writing the binary dictionary.
SettingsValues.setLastUserHistoryWriteTime(mPrefs, mLocale);
if (PROFILE_SAVE_RESTORE) {
final long diff = System.currentTimeMillis() - now;
- Log.w(TAG, "PROF: Write User HistoryDictionary: " + mLocale + ", "+ diff
- + "ms. Total: " + profTotal + ". Insert: " + profInsert + ". Delete: "
- + profDelete);
+ Log.w(TAG, "PROF: Write User HistoryDictionary: " + mLocale + ", " + diff + "ms.");
}
- db.setTransactionSuccessful();
- return null;
}
- private static ContentValues getContentValues(String word1, String word2, String locale) {
- ContentValues values = new ContentValues(3);
- values.put(MAIN_COLUMN_WORD1, word1);
- values.put(MAIN_COLUMN_WORD2, word2);
- values.put(MAIN_COLUMN_LOCALE, locale);
- return values;
+ @Override
+ public int getFrequency(String word1, 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 = ((int)fc) & 0xFF;
+ } else if (UserHistoryForgettingCurveUtils.
+ needsToSave(fc, isValid, mAddLevel0Bigrams)) {
+ freq = ((int)fc) & 0xFF;
+ } else {
+ // Delete this entry
+ freq = -1;
+ }
+ } else {
+ // Delete this entry
+ freq = -1;
+ }
+ }
+ return freq;
}
+ }
- private static ContentValues getFrequencyContentValues(int pairId, int frequency) {
- ContentValues values = new ContentValues(2);
- values.put(FREQ_COLUMN_PAIR_ID, pairId);
- values.put(COLUMN_FORGETTING_CURVE_VALUE, frequency);
- return values;
+ 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/UserHistoryDictionaryBigramList.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
index 28847745e..df44948f9 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionaryBigramList.java
@@ -26,12 +26,11 @@ import java.util.Set;
* All bigrams including stale ones in SQL DB should be stored in this class to avoid adding stale
* bigrams when we write to the SQL DB.
*/
-public class UserHistoryDictionaryBigramList {
+public final class UserHistoryDictionaryBigramList {
public static final byte FORGETTING_CURVE_INITIAL_VALUE = 0;
private static final String TAG = UserHistoryDictionaryBigramList.class.getSimpleName();
- private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = new HashMap<String, Byte>();
- private final HashMap<String, HashMap<String, Byte>> mBigramMap =
- new HashMap<String, HashMap<String, Byte>>();
+ private static final HashMap<String, Byte> EMPTY_BIGRAM_MAP = CollectionUtils.newHashMap();
+ private final HashMap<String, HashMap<String, Byte>> mBigramMap = CollectionUtils.newHashMap();
private int mSize = 0;
public void evictAll() {
@@ -57,7 +56,7 @@ public class UserHistoryDictionaryBigramList {
if (mBigramMap.containsKey(word1)) {
map = mBigramMap.get(word1);
} else {
- map = new HashMap<String, Byte>();
+ map = CollectionUtils.newHashMap();
mBigramMap.put(word1, map);
}
if (!map.containsKey(word2)) {
@@ -98,11 +97,11 @@ public class UserHistoryDictionaryBigramList {
}
public HashMap<String, Byte> getBigrams(String word1) {
- if (!mBigramMap.containsKey(word1)) {
- return EMPTY_BIGRAM_MAP;
- } else {
- return mBigramMap.get(word1);
- }
+ if (mBigramMap.containsKey(word1)) return mBigramMap.get(word1);
+ // TODO: lower case according to locale
+ final String lowerWord1 = word1.toLowerCase();
+ if (mBigramMap.containsKey(lowerWord1)) return mBigramMap.get(lowerWord1);
+ return EMPTY_BIGRAM_MAP;
}
public boolean removeBigram(String word1, String word2) {
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
index e5516dc62..9053d709b 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryForgettingCurveUtils.java
@@ -19,7 +19,7 @@ package com.android.inputmethod.latin;
import android.text.format.DateUtils;
import android.util.Log;
-public class UserHistoryForgettingCurveUtils {
+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;
@@ -36,7 +36,7 @@ public class UserHistoryForgettingCurveUtils {
// This utility class is not publicly instantiable.
}
- public static class ForgettingCurveParams {
+ public static final class ForgettingCurveParams {
private byte mFc;
long mLastTouchedTime = 0;
private final boolean mIsValid;
@@ -50,7 +50,7 @@ public class UserHistoryForgettingCurveUtils {
}
private ForgettingCurveParams(long now, boolean isValid) {
- this((int)pushCount((byte)0, isValid), now, now, isValid);
+ this(pushCount((byte)0, isValid), now, now, isValid);
}
/** This constructor is called when the user history bigram dictionary is being restored. */
@@ -195,24 +195,24 @@ public class UserHistoryForgettingCurveUtils {
return (elapsedTime < ELAPSED_TIME_MAX - 1 || level > 0);
}
- private static class MathUtils {
+ private static final class MathUtils {
public static final int[][] SCORE_TABLE = new int[FC_LEVEL_MAX][ELAPSED_TIME_MAX + 1];
static {
for (int i = 0; i < FC_LEVEL_MAX; ++i) {
- final double initialFreq;
+ final float initialFreq;
if (i >= 2) {
- initialFreq = (double)FC_FREQ_MAX;
+ initialFreq = FC_FREQ_MAX;
} else if (i == 1) {
- initialFreq = (double)FC_FREQ_MAX / 2;
+ initialFreq = FC_FREQ_MAX / 2;
} else if (i == 0) {
- initialFreq = (double)FC_FREQ_MAX / 4;
+ initialFreq = FC_FREQ_MAX / 4;
} else {
continue;
}
for (int j = 0; j < ELAPSED_TIME_MAX; ++j) {
- final double elapsedHour = j * ELAPSED_TIME_INTERVAL_HOURS;
- final double freq =
- initialFreq * Math.pow(initialFreq, elapsedHour / HALF_LIFE_HOURS);
+ final float elapsedHours = j * ELAPSED_TIME_INTERVAL_HOURS;
+ final float freq = initialFreq
+ * (float)Math.pow(initialFreq, elapsedHours / HALF_LIFE_HOURS);
final int intFreq = Math.min(FC_FREQ_MAX, Math.max(0, (int)freq));
SCORE_TABLE[i][j] = intFreq;
}
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index 4178955bc..876bc8e79 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -16,20 +16,16 @@
package com.android.inputmethod.latin;
-import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
-import android.content.res.Resources;
import android.inputmethodservice.InputMethodService;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Process;
import android.text.TextUtils;
-import android.text.format.DateUtils;
import android.util.Log;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
@@ -44,12 +40,9 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
-import java.util.Collections;
import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-public class Utils {
+public final class Utils {
private Utils() {
// This utility class is not publicly instantiable.
}
@@ -67,44 +60,6 @@ public class Utils {
}
}
- public static class GCUtils {
- private static final String GC_TAG = GCUtils.class.getSimpleName();
- public static final int GC_TRY_COUNT = 2;
- // GC_TRY_LOOP_MAX is used for the hard limit of GC wait,
- // GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT.
- public static final int GC_TRY_LOOP_MAX = 5;
- private static final long GC_INTERVAL = DateUtils.SECOND_IN_MILLIS;
- private static GCUtils sInstance = new GCUtils();
- private int mGCTryCount = 0;
-
- public static GCUtils getInstance() {
- return sInstance;
- }
-
- public void reset() {
- mGCTryCount = 0;
- }
-
- public boolean tryGCOrWait(String metaData, Throwable t) {
- if (mGCTryCount == 0) {
- System.gc();
- }
- if (++mGCTryCount > GC_TRY_COUNT) {
- LatinImeLogger.logOnException(metaData, t);
- return false;
- } else {
- try {
- Thread.sleep(GC_INTERVAL);
- return true;
- } catch (InterruptedException e) {
- Log.e(GC_TAG, "Sleep was interrupted.");
- LatinImeLogger.logOnException(metaData, t);
- return false;
- }
- }
- }
- }
-
/* package */ static class RingCharBuffer {
private static RingCharBuffer sRingCharBuffer = new RingCharBuffer();
private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
@@ -206,19 +161,25 @@ public class Utils {
}
// Get the current stack trace
- public static String getStackTrace() {
+ public static String getStackTrace(final int limit) {
StringBuilder sb = new StringBuilder();
try {
throw new RuntimeException();
} catch (RuntimeException e) {
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) sb.append(frames[j].toString() + "\n");
+ for (int j = 1; j < frames.length && j < limit + 1; ++j) {
+ sb.append(frames[j].toString() + "\n");
+ }
}
return sb.toString();
}
- public static class UsabilityStudyLogUtils {
+ 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";
@@ -427,34 +388,48 @@ public class Utils {
}
}
- public static float getDipScale(Context context) {
- final float scale = context.getResources().getDisplayMetrics().density;
- return scale;
- }
-
- /** Convert pixel to DIP */
- public static int dipToPixel(float scale, int dip) {
- return (int) (dip * scale + 0.5);
- }
-
- public static class Stats {
+ 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) {
- // TODO: accept code points
- RingCharBuffer.getInstance().push((char)code, x, y);
+ 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 int separatorCode) {
- if (TextUtils.isEmpty(typedWord)) return;
- LatinImeLogger.logOnAutoCorrection(typedWord, correctedWord, separatorCode);
+ 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() {
@@ -470,60 +445,4 @@ public class Utils {
if (TextUtils.isEmpty(info)) return null;
return info;
}
-
- private static final String HARDWARE_PREFIX = Build.HARDWARE + ",";
- private static final HashMap<String, String> sDeviceOverrideValueMap =
- new HashMap<String, String>();
-
- public static String getDeviceOverrideValue(Resources res, int overrideResId, String defValue) {
- final int orientation = res.getConfiguration().orientation;
- final String key = overrideResId + "-" + orientation;
- if (!sDeviceOverrideValueMap.containsKey(key)) {
- String overrideValue = defValue;
- for (final String element : res.getStringArray(overrideResId)) {
- if (element.startsWith(HARDWARE_PREFIX)) {
- overrideValue = element.substring(HARDWARE_PREFIX.length());
- break;
- }
- }
- sDeviceOverrideValueMap.put(key, overrideValue);
- }
- return sDeviceOverrideValueMap.get(key);
- }
-
- private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = new HashMap<String, Long>();
- private static final String LOCALE_AND_TIME_STR_SEPARATER = ",";
- public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) {
- if (TextUtils.isEmpty(str)) {
- return EMPTY_LT_HASH_MAP;
- }
- final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER);
- final int N = ss.length;
- if (N < 2 || N % 2 != 0) {
- return EMPTY_LT_HASH_MAP;
- }
- final HashMap<String, Long> retval = new HashMap<String, Long>();
- for (int i = 0; i < N / 2; ++i) {
- final String localeStr = ss[i * 2];
- final long time = Long.valueOf(ss[i * 2 + 1]);
- retval.put(localeStr, time);
- }
- return retval;
- }
-
- public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) {
- if (map == null || map.isEmpty()) {
- return "";
- }
- final StringBuilder builder = new StringBuilder();
- for (String localeStr : map.keySet()) {
- if (builder.length() > 0) {
- builder.append(LOCALE_AND_TIME_STR_SEPARATER);
- }
- final Long time = map.get(localeStr);
- builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER);
- builder.append(String.valueOf(time));
- }
- return builder.toString();
- }
}
diff --git a/java/src/com/android/inputmethod/latin/VibratorUtils.java b/java/src/com/android/inputmethod/latin/VibratorUtils.java
index 33ffdd9c9..b6696cec0 100644
--- a/java/src/com/android/inputmethod/latin/VibratorUtils.java
+++ b/java/src/com/android/inputmethod/latin/VibratorUtils.java
@@ -19,7 +19,7 @@ package com.android.inputmethod.latin;
import android.content.Context;
import android.os.Vibrator;
-public class VibratorUtils {
+public final class VibratorUtils {
private static final VibratorUtils sInstance = new VibratorUtils();
private Vibrator mVibrator;
diff --git a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
deleted file mode 100644
index a0de2f970..000000000
--- a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
+++ /dev/null
@@ -1,109 +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;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.Pair;
-
-import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
-
-import java.util.HashMap;
-import java.util.Locale;
-
-public class WhitelistDictionary extends ExpandableDictionary {
-
- private static final boolean DBG = LatinImeLogger.sDBG;
- private static final String TAG = WhitelistDictionary.class.getSimpleName();
-
- private final HashMap<String, Pair<Integer, String>> mWhitelistWords =
- new HashMap<String, Pair<Integer, String>>();
-
- // TODO: Conform to the async load contact of ExpandableDictionary
- public WhitelistDictionary(final Context context, final Locale locale) {
- super(context, Suggest.DIC_WHITELIST);
- // TODO: Move whitelist dictionary into main dictionary.
- final RunInLocale<Void> job = new RunInLocale<Void>() {
- @Override
- protected Void job(Resources res) {
- initWordlist(res.getStringArray(R.array.wordlist_whitelist));
- return null;
- }
- };
- job.runInLocale(context.getResources(), locale);
- }
-
- private void initWordlist(String[] wordlist) {
- mWhitelistWords.clear();
- final int N = wordlist.length;
- if (N % 3 != 0) {
- if (DBG) {
- Log.d(TAG, "The number of the whitelist is invalid.");
- }
- return;
- }
- try {
- for (int i = 0; i < N; i += 3) {
- final int score = Integer.valueOf(wordlist[i]);
- final String before = wordlist[i + 1];
- final String after = wordlist[i + 2];
- if (before != null && after != null) {
- mWhitelistWords.put(
- before.toLowerCase(), new Pair<Integer, String>(score, after));
- addWord(after, null /* shortcut */, score);
- }
- }
- } catch (NumberFormatException e) {
- if (DBG) {
- Log.d(TAG, "The score of the word is invalid.");
- }
- }
- }
-
- public String getWhitelistedWord(String before) {
- if (before == null) return null;
- final String lowerCaseBefore = before.toLowerCase();
- if(mWhitelistWords.containsKey(lowerCaseBefore)) {
- if (DBG) {
- Log.d(TAG, "--- found whitelistedWord: " + lowerCaseBefore);
- }
- return mWhitelistWords.get(lowerCaseBefore).second;
- }
- return null;
- }
-
- // See LatinIME#updateSuggestions. This breaks in the (queer) case that the whitelist
- // lists that word a should autocorrect to word b, and word c would autocorrect to
- // an upper-cased version of a. In this case, the way this return value is used would
- // remove the first candidate when the user typed the upper-cased version of A.
- // Example : abc -> def and xyz -> Abc
- // A user typing Abc would experience it being autocorrected to something else (not
- // necessarily def).
- // There is no such combination in the whitelist at the time and there probably won't
- // ever be - it doesn't make sense. But still.
- public boolean shouldForciblyAutoCorrectFrom(CharSequence word) {
- if (TextUtils.isEmpty(word)) return false;
- final String correction = getWhitelistedWord(word.toString());
- if (TextUtils.isEmpty(correction)) return false;
- return !correction.equals(word);
- }
-
- // Leave implementation of getWords and isValidWord to the superclass.
- // The words have been added to the ExpandableDictionary with addWord() inside initWordlist.
-}
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index ca9caa1d3..da0071adc 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -17,32 +17,35 @@
package com.android.inputmethod.latin;
import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardActionListener;
import java.util.Arrays;
/**
* A place to store the currently composing word with information such as adjacent key codes as well
*/
-public class WordComposer {
-
- public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE;
- public static final int NOT_A_COORDINATE = -1;
-
+public final class WordComposer {
private static final int N = BinaryDictionary.MAX_WORD_LENGTH;
+ public static final int CAPS_MODE_OFF = 0;
+ // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits
+ // aren't used anywhere in the code
+ public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1;
+ public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3;
+ public static final int CAPS_MODE_AUTO_SHIFTED = 0x5;
+ public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7;
+
private int[] mPrimaryKeyCodes;
- private int[] mXCoordinates;
- private int[] mYCoordinates;
- private StringBuilder mTypedWord;
+ private final InputPointers mInputPointers = new InputPointers(N);
+ private final StringBuilder mTypedWord;
private CharSequence mAutoCorrection;
private boolean mIsResumed;
+ private boolean mIsBatchMode;
// Cache these values for performance
private int mCapsCount;
- private boolean mAutoCapitalized;
+ private int mDigitsCount;
+ private int mCapitalizedMode;
private int mTrailingSingleQuotesCount;
private int mCodePointSize;
@@ -54,28 +57,24 @@ public class WordComposer {
public WordComposer() {
mPrimaryKeyCodes = new int[N];
mTypedWord = new StringBuilder(N);
- mXCoordinates = new int[N];
- mYCoordinates = new int[N];
mAutoCorrection = null;
mTrailingSingleQuotesCount = 0;
mIsResumed = false;
+ mIsBatchMode = false;
refreshSize();
}
public WordComposer(WordComposer source) {
- init(source);
- }
-
- public void init(WordComposer source) {
mPrimaryKeyCodes = Arrays.copyOf(source.mPrimaryKeyCodes, source.mPrimaryKeyCodes.length);
mTypedWord = new StringBuilder(source.mTypedWord);
- mXCoordinates = Arrays.copyOf(source.mXCoordinates, source.mXCoordinates.length);
- mYCoordinates = Arrays.copyOf(source.mYCoordinates, source.mYCoordinates.length);
+ mInputPointers.copy(source.mInputPointers);
mCapsCount = source.mCapsCount;
+ mDigitsCount = source.mDigitsCount;
mIsFirstCharCapitalized = source.mIsFirstCharCapitalized;
- mAutoCapitalized = source.mAutoCapitalized;
+ mCapitalizedMode = source.mCapitalizedMode;
mTrailingSingleQuotesCount = source.mTrailingSingleQuotesCount;
mIsResumed = source.mIsResumed;
+ mIsBatchMode = source.mIsBatchMode;
refreshSize();
}
@@ -86,13 +85,15 @@ public class WordComposer {
mTypedWord.setLength(0);
mAutoCorrection = null;
mCapsCount = 0;
+ mDigitsCount = 0;
mIsFirstCharCapitalized = false;
mTrailingSingleQuotesCount = 0;
mIsResumed = false;
+ mIsBatchMode = false;
refreshSize();
}
- public final void refreshSize() {
+ private final void refreshSize() {
mCodePointSize = mTypedWord.codePointCount(0, mTypedWord.length());
}
@@ -116,12 +117,8 @@ public class WordComposer {
return mPrimaryKeyCodes[index];
}
- public int[] getXCoordinates() {
- return mXCoordinates;
- }
-
- public int[] getYCoordinates() {
- return mYCoordinates;
+ public InputPointers getInputPointers() {
+ return mInputPointers;
}
private static boolean isFirstCharCapitalized(int index, int codePoint, boolean previous) {
@@ -129,40 +126,28 @@ public class WordComposer {
return previous && !Character.isUpperCase(codePoint);
}
- // TODO: remove input keyDetector
- public void add(int primaryCode, int x, int y, KeyDetector keyDetector) {
- final int keyX;
- final int keyY;
- if (null == keyDetector
- || x == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
- || y == KeyboardActionListener.SUGGESTION_STRIP_COORDINATE
- || x == KeyboardActionListener.NOT_A_TOUCH_COORDINATE
- || y == KeyboardActionListener.NOT_A_TOUCH_COORDINATE) {
- keyX = x;
- keyY = y;
- } else {
- keyX = keyDetector.getTouchX(x);
- keyY = keyDetector.getTouchY(y);
- }
- add(primaryCode, keyX, keyY);
- }
-
/**
* Add a new keystroke, with the pressed key's code point with the touch point coordinates.
*/
- private void add(int primaryCode, int keyX, int keyY) {
+ public void add(int primaryCode, int keyX, int keyY) {
final int newIndex = size();
mTypedWord.appendCodePoint(primaryCode);
refreshSize();
if (newIndex < BinaryDictionary.MAX_WORD_LENGTH) {
mPrimaryKeyCodes[newIndex] = primaryCode >= Keyboard.CODE_SPACE
? Character.toLowerCase(primaryCode) : primaryCode;
- mXCoordinates[newIndex] = keyX;
- mYCoordinates[newIndex] = keyY;
+ // In the batch input mode, the {@code mInputPointers} holds batch input points and
+ // shouldn't be overridden by the "typed key" coordinates
+ // (See {@link #setBatchInputWord}).
+ if (!mIsBatchMode) {
+ // TODO: Set correct pointer id and time
+ mInputPointers.addPointer(newIndex, keyX, keyY, 0, 0);
+ }
}
mIsFirstCharCapitalized = isFirstCharCapitalized(
newIndex, primaryCode, mIsFirstCharCapitalized);
if (Character.isUpperCase(primaryCode)) mCapsCount++;
+ if (Character.isDigit(primaryCode)) mDigitsCount++;
if (Keyboard.CODE_SINGLE_QUOTE == primaryCode) {
++mTrailingSingleQuotesCount;
} else {
@@ -171,19 +156,37 @@ public class WordComposer {
mAutoCorrection = null;
}
+ public void setBatchInputPointers(InputPointers batchPointers) {
+ mInputPointers.set(batchPointers);
+ mIsBatchMode = true;
+ }
+
+ public void setBatchInputWord(CharSequence word) {
+ reset();
+ mIsBatchMode = true;
+ final int length = word.length();
+ for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
+ final int codePoint = Character.codePointAt(word, i);
+ // We don't want to override the batch input points that are held in mInputPointers
+ // (See {@link #add(int,int,int)}).
+ add(codePoint, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+ }
+ }
+
/**
* Internal method to retrieve reasonable proximity info for a character.
*/
private void addKeyInfo(final int codePoint, final Keyboard keyboard) {
- for (final Key key : keyboard.mKeys) {
- if (key.mCode == codePoint) {
- final int x = key.mX + key.mWidth / 2;
- final int y = key.mY + key.mHeight / 2;
- add(codePoint, x, y);
- return;
- }
+ 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;
+ } else {
+ x = Constants.NOT_A_COORDINATE;
+ y = Constants.NOT_A_COORDINATE;
}
- add(codePoint, WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
+ add(codePoint, x, y);
}
/**
@@ -194,7 +197,7 @@ public class WordComposer {
reset();
final int length = word.length();
for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
- int codePoint = Character.codePointAt(word, i);
+ final int codePoint = Character.codePointAt(word, i);
addKeyInfo(codePoint, keyboard);
}
mIsResumed = true;
@@ -219,6 +222,7 @@ public class WordComposer {
mTypedWord.deleteCharAt(stringBuilderLength - 1);
}
if (Character.isUpperCase(lastChar)) mCapsCount--;
+ if (Character.isDigit(lastChar)) mDigitsCount--;
refreshSize();
}
// We may have deleted the last one.
@@ -263,7 +267,17 @@ public class WordComposer {
* @return true if all user typed chars are upper case, false otherwise
*/
public boolean isAllUpperCase() {
- return (mCapsCount > 0) && (mCapsCount == size());
+ if (size() <= 1) {
+ return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
+ || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED;
+ } else {
+ return mCapsCount == size();
+ }
+ }
+
+ public boolean wasShiftedNoLock() {
+ return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED
+ || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED;
}
/**
@@ -274,20 +288,34 @@ public class WordComposer {
}
/**
- * Saves the reason why the word is capitalized - whether it was automatic or
- * due to the user hitting shift in the middle of a sentence.
- * @param auto whether it was an automatic capitalization due to start of sentence
+ * Returns true if we have digits in the composing word.
*/
- public void setAutoCapitalized(boolean auto) {
- mAutoCapitalized = auto;
+ public boolean hasDigits() {
+ return mDigitsCount > 0;
+ }
+
+ /**
+ * Saves the caps mode at the start of composing.
+ *
+ * WordComposer needs to know about this for several reasons. The first is, we need to know
+ * after the fact what the reason was, to register the correct form into the user history
+ * dictionary: if the word was automatically capitalized, we should insert it in all-lower
+ * case but if it's a manual pressing of shift, then it should be inserted as is.
+ * Also, batch input needs to know about the current caps mode to display correctly
+ * capitalized suggestions.
+ * @param mode the mode at the time of start
+ */
+ public void setCapitalizedModeAtStartComposingTime(final int mode) {
+ mCapitalizedMode = mode;
}
/**
* Returns whether the word was automatically capitalized.
* @return whether the word was automatically capitalized
*/
- public boolean isAutoCapitalized() {
- return mAutoCapitalized;
+ public boolean wasAutoCapitalized() {
+ return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
+ || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED;
}
/**
@@ -313,24 +341,26 @@ public class WordComposer {
// `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
public LastComposedWord commitWord(final int type, final String committedWord,
- final int separatorCode, final CharSequence prevWord) {
+ final String separatorString, final CharSequence prevWord) {
// Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
// or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
// the last composed word to ensure this does not happen.
final int[] primaryKeyCodes = mPrimaryKeyCodes;
- final int[] xCoordinates = mXCoordinates;
- final int[] yCoordinates = mYCoordinates;
mPrimaryKeyCodes = new int[N];
- mXCoordinates = new int[N];
- mYCoordinates = new int[N];
final LastComposedWord lastComposedWord = new LastComposedWord(primaryKeyCodes,
- xCoordinates, yCoordinates, mTypedWord.toString(), committedWord, separatorCode,
+ mInputPointers, mTypedWord.toString(), committedWord, separatorString,
prevWord);
+ mInputPointers.reset();
if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
&& type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
lastComposedWord.deactivate();
}
+ mCapsCount = 0;
+ mDigitsCount = 0;
+ mIsBatchMode = false;
mTypedWord.setLength(0);
+ mTrailingSingleQuotesCount = 0;
+ mIsFirstCharCapitalized = false;
refreshSize();
mAutoCorrection = null;
mIsResumed = false;
@@ -339,12 +369,15 @@ public class WordComposer {
public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
- mXCoordinates = lastComposedWord.mXCoordinates;
- mYCoordinates = lastComposedWord.mYCoordinates;
+ mInputPointers.set(lastComposedWord.mInputPointers);
mTypedWord.setLength(0);
mTypedWord.append(lastComposedWord.mTypedWord);
refreshSize();
mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
mIsResumed = true;
}
+
+ public boolean isBatchMode() {
+ return mIsBatchMode;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/WordListInfo.java b/java/src/com/android/inputmethod/latin/WordListInfo.java
index 54f04d78f..095320e25 100644
--- a/java/src/com/android/inputmethod/latin/WordListInfo.java
+++ b/java/src/com/android/inputmethod/latin/WordListInfo.java
@@ -19,7 +19,7 @@ package com.android.inputmethod.latin;
/**
* Information container for a word list.
*/
-public class WordListInfo {
+public final class WordListInfo {
public final String mId;
public final String mLocale;
public WordListInfo(final String id, final String locale) {
diff --git a/java/src/com/android/inputmethod/latin/XmlParseUtils.java b/java/src/com/android/inputmethod/latin/XmlParseUtils.java
index 481cdfa47..75dc68f1d 100644
--- a/java/src/com/android/inputmethod/latin/XmlParseUtils.java
+++ b/java/src/com/android/inputmethod/latin/XmlParseUtils.java
@@ -23,7 +23,7 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
-public class XmlParseUtils {
+public final class XmlParseUtils {
private XmlParseUtils() {
// This utility class is not publicly instantiable.
}
@@ -36,28 +36,28 @@ public class XmlParseUtils {
}
@SuppressWarnings("serial")
- public static class IllegalStartTag extends ParseException {
+ public static final class IllegalStartTag extends ParseException {
public IllegalStartTag(XmlPullParser parser, String parent) {
super("Illegal start tag " + parser.getName() + " in " + parent, parser);
}
}
@SuppressWarnings("serial")
- public static class IllegalEndTag extends ParseException {
+ public static final class IllegalEndTag extends ParseException {
public IllegalEndTag(XmlPullParser parser, String parent) {
super("Illegal end tag " + parser.getName() + " in " + parent, parser);
}
}
@SuppressWarnings("serial")
- public static class IllegalAttribute extends ParseException {
+ public static final class IllegalAttribute extends ParseException {
public IllegalAttribute(XmlPullParser parser, String attribute) {
super("Tag " + parser.getName() + " has illegal attribute " + attribute, parser);
}
}
@SuppressWarnings("serial")
- public static class NonEmptyTag extends ParseException{
+ public static final class NonEmptyTag extends ParseException{
public NonEmptyTag(String tag, XmlPullParser parser) {
super(tag + " must be empty tag", parser);
}
diff --git a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java
index de2057669..52c066a44 100644
--- a/java/src/com/android/inputmethod/latin/define/ProductionFlag.java
+++ b/java/src/com/android/inputmethod/latin/define/ProductionFlag.java
@@ -22,4 +22,5 @@ public final class ProductionFlag {
}
public static final boolean IS_EXPERIMENTAL = false;
+ public static final boolean IS_INTERNAL = false;
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
new file mode 100644
index 000000000..7b0231a6b
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictIOUtils.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin.makedict;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.makedict.BinaryDictInputOutput.FusionDictionaryBufferInterface;
+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 java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Stack;
+
+public final class BinaryDictIOUtils {
+ private static final boolean DBG = false;
+
+ private static final class Position {
+ public static final int NOT_READ_GROUPCOUNT = -1;
+
+ public int mAddress;
+ public int mNumOfCharGroup;
+ public int mPosition;
+ public int mLength;
+
+ public Position(int address, int length) {
+ mAddress = address;
+ mLength = length;
+ mNumOfCharGroup = NOT_READ_GROUPCOUNT;
+ }
+ }
+
+ /**
+ * Tours all node without recursive call.
+ */
+ private static void readUnigramsAndBigramsBinaryInner(
+ final FusionDictionaryBufferInterface buffer, final int headerSize,
+ final Map<Integer, String> words, final Map<Integer, Integer> frequencies,
+ final Map<Integer, ArrayList<PendingAttribute>> bigrams,
+ final FormatOptions formatOptions) {
+ int[] pushedChars = new int[FormatSpec.MAX_WORD_LENGTH + 1];
+
+ Stack<Position> stack = new Stack<Position>();
+ int index = 0;
+
+ Position initPos = new Position(headerSize, 0);
+ stack.push(initPos);
+
+ while (!stack.empty()) {
+ Position p = stack.peek();
+
+ if (DBG) {
+ MakedictLog.d("read: address=" + p.mAddress + ", numOfCharGroup=" +
+ p.mNumOfCharGroup + ", position=" + p.mPosition + ", length=" + p.mLength);
+ }
+
+ if (buffer.position() != p.mAddress) buffer.position(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);
+ p.mPosition = 0;
+ }
+ if (p.mNumOfCharGroup == 0) {
+ stack.pop();
+ continue;
+ }
+ CharGroupInfo info = BinaryDictInputOutput.readCharGroup(buffer,
+ p.mAddress - headerSize, formatOptions);
+ for (int i = 0; i < info.mCharacters.length; ++i) {
+ pushedChars[index++] = info.mCharacters[i];
+ }
+ p.mPosition++;
+
+ final boolean isMovedGroup = BinaryDictInputOutput.isMovedGroup(info.mFlags,
+ formatOptions);
+ if (!isMovedGroup
+ && info.mFrequency != FusionDictionary.CharGroup.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 (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;
+ } else {
+ stack.pop();
+ }
+ } else {
+ stack.pop();
+ }
+ } else {
+ // the node has more groups.
+ p.mAddress = buffer.position();
+ }
+
+ if (!isMovedGroup && BinaryDictInputOutput.hasChildrenAddress(info.mChildrenAddress)) {
+ Position childrenPos = new Position(info.mChildrenAddress + headerSize, index);
+ stack.push(childrenPos);
+ }
+ }
+ }
+
+ /**
+ * Reads unigrams and bigrams from the binary file.
+ * Doesn't make the memory representation of the dictionary.
+ *
+ * @param buffer the buffer to read.
+ * @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
+ */
+ public static void readUnigramsAndBigramsBinary(final FusionDictionaryBufferInterface buffer,
+ 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);
+ }
+
+ /**
+ * Gets the address of the last CharGroup of the exact matching word in the dictionary.
+ * If no match is found, returns NOT_VALID_WORD.
+ *
+ * @param buffer the buffer to read.
+ * @param word the word we search for.
+ * @return the address of the terminal node.
+ * @throws IOException
+ * @throws UnsupportedFormatException
+ */
+ public static int getTerminalPosition(final FusionDictionaryBufferInterface buffer,
+ final String word) throws IOException, UnsupportedFormatException {
+ if (word == null) return FormatSpec.NOT_VALID_WORD;
+ if (buffer.position() != 0) buffer.position(0);
+
+ final FileHeader header = BinaryDictInputOutput.readHeader(buffer);
+ int wordPos = 0;
+ final int wordLen = word.codePointCount(0, word.length());
+ for (int depth = 0; depth < Constants.Dictionary.MAX_WORD_LENGTH; ++depth) {
+ if (wordPos >= wordLen) return FormatSpec.NOT_VALID_WORD;
+
+ do {
+ int groupOffset = buffer.position() - header.mHeaderSize;
+ final int charGroupCount = BinaryDictInputOutput.readCharGroupCount(buffer);
+ groupOffset += BinaryDictInputOutput.getGroupCountSize(charGroupCount);
+
+ 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);
+ if (BinaryDictInputOutput.isMovedGroup(currentInfo.mFlags,
+ header.mFormatOptions)) {
+ continue;
+ }
+ boolean same = true;
+ for (int p = 0, j = word.offsetByCodePoints(0, wordPos);
+ p < currentInfo.mCharacters.length;
+ ++p, j = word.offsetByCodePoints(j, 1)) {
+ if (wordPos + p >= wordLen
+ || word.codePointAt(j) != currentInfo.mCharacters[p]) {
+ same = false;
+ break;
+ }
+ }
+
+ if (same) {
+ // found the group matches the word.
+ if (wordPos + currentInfo.mCharacters.length == wordLen) {
+ if (currentInfo.mFrequency == CharGroup.NOT_A_TERMINAL) {
+ return FormatSpec.NOT_VALID_WORD;
+ } else {
+ return charGroupPos;
+ }
+ }
+ wordPos += currentInfo.mCharacters.length;
+ if (currentInfo.mChildrenAddress == FormatSpec.NO_CHILDREN_ADDRESS) {
+ return FormatSpec.NOT_VALID_WORD;
+ }
+ foundNextCharGroup = true;
+ buffer.position(currentInfo.mChildrenAddress);
+ break;
+ }
+ groupOffset = currentInfo.mEndAddress;
+ }
+
+ // 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
+ // a forward link address that we need to consult and possibly resume
+ // search on the next node in the linked list.
+ if (foundNextCharGroup) break;
+ if (!header.mFormatOptions.mSupportsDynamicUpdate) {
+ return FormatSpec.NOT_VALID_WORD;
+ }
+
+ final int forwardLinkAddress = buffer.readUnsignedInt24();
+ if (forwardLinkAddress == FormatSpec.NO_FORWARD_LINK_ADDRESS) {
+ return FormatSpec.NOT_VALID_WORD;
+ }
+ buffer.position(forwardLinkAddress);
+ } while(true);
+ }
+ return FormatSpec.NOT_VALID_WORD;
+ }
+
+ /**
+ * Delete the word from the binary file.
+ *
+ * @param buffer the buffer to write.
+ * @param word the word we delete
+ * @throws IOException
+ * @throws UnsupportedFormatException
+ */
+ 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();
+ final int newFlags = flags ^ FormatSpec.FLAG_IS_TERMINAL;
+ buffer.position(wordPosition);
+ buffer.put((byte)newFlags);
+ }
+
+ private static void putSInt24(final FusionDictionaryBufferInterface buffer,
+ 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));
+ }
+
+ /**
+ * Update a parent address in a CharGroup that is addressed 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();
+ final int parentOffset = newParentAddress - groupOriginAddress;
+ putSInt24(buffer, parentOffset);
+ buffer.position(originalPosition);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
index 89c59f809..b431a4da9 100644
--- a/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
+++ b/java/src/com/android/inputmethod/latin/makedict/BinaryDictInputOutput.java
@@ -16,16 +16,21 @@
package com.android.inputmethod.latin.makedict;
+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.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -38,145 +43,9 @@ import java.util.TreeMap;
*
* All the methods in this class are static.
*/
-public class BinaryDictInputOutput {
-
- final static boolean DBG = MakedictLog.DBG;
-
- /* Node layout is as follows:
- * | 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
- * g | has several chars ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_MULTIPLE_CHARS
- * s | 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
- * | has bigrams ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_BIGRAMS
- *
- * c | IF FLAG_HAS_MULTIPLE_CHARS
- * h | char, char, char, char n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers
- * a | end 1 byte, = 0
- * r | ELSE
- * s | char 1 or 3 bytes
- * | END
- *
- * f |
- * r | IF FLAG_IS_TERMINAL
- * 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
- *
- * | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS
- * | shortcut string list
- * | IF FLAG_IS_TERMINAL && FLAG_HAS_BIGRAMS
- * | bigrams address list
- *
- * 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).
- *
- * bigram address list is:
- * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
- * | addressSign = 1 bit, : FLAG_ATTRIBUTE_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)
- * | read 1 byte, add top 4 bits
- * | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat)
- * | read 2 bytes, add top 4 bits
- * | ELSE // 11 == FLAG_ATTRIBUTE_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
- *
- * 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
- * | reserved = 3 bits, must be 0
- * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_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
- */
+public final class BinaryDictInputOutput {
- private static final int VERSION_1_MAGIC_NUMBER = 0x78B1;
- private static final int VERSION_2_MAGIC_NUMBER = 0x9BC13AFE;
- private static final int MINIMUM_SUPPORTED_VERSION = 1;
- private static final int MAXIMUM_SUPPORTED_VERSION = 2;
- private static final int NOT_A_VERSION_NUMBER = -1;
- private static final int FIRST_VERSION_WITH_HEADER_SIZE = 2;
-
- // These options need to be the same numeric values as the one in the native reading code.
- private static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1;
- private static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4;
- private static final int CONTAINS_BIGRAMS_FLAG = 0x8;
-
- // TODO: Make this value adaptative to content data, store it in the header, and
- // use it in the reading code.
- private static final int MAX_WORD_LENGTH = 48;
-
- private static final int MASK_GROUP_ADDRESS_TYPE = 0xC0;
- private static final int FLAG_GROUP_ADDRESS_TYPE_NOADDRESS = 0x00;
- private static final int FLAG_GROUP_ADDRESS_TYPE_ONEBYTE = 0x40;
- private static final int FLAG_GROUP_ADDRESS_TYPE_TWOBYTES = 0x80;
- private static final int FLAG_GROUP_ADDRESS_TYPE_THREEBYTES = 0xC0;
-
- private static final int FLAG_HAS_MULTIPLE_CHARS = 0x20;
-
- private static final int FLAG_IS_TERMINAL = 0x10;
- private static final int FLAG_HAS_SHORTCUT_TARGETS = 0x08;
- private static final int FLAG_HAS_BIGRAMS = 0x04;
-
- private static final int FLAG_ATTRIBUTE_HAS_NEXT = 0x80;
- private static final int FLAG_ATTRIBUTE_OFFSET_NEGATIVE = 0x40;
- private static final int MASK_ATTRIBUTE_ADDRESS_TYPE = 0x30;
- private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE = 0x10;
- private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
- private static final int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
- private static final int FLAG_ATTRIBUTE_FREQUENCY = 0x0F;
-
- private static final int GROUP_CHARACTERS_TERMINATOR = 0x1F;
-
- private static final int GROUP_TERMINATOR_SIZE = 1;
- private static final int GROUP_FLAGS_SIZE = 1;
- private static final int GROUP_FREQUENCY_SIZE = 1;
- private static final int GROUP_MAX_ADDRESS_SIZE = 3;
- private static final int GROUP_ATTRIBUTE_FLAGS_SIZE = 1;
- private static final int GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE = 3;
- private static final int GROUP_SHORTCUT_LIST_SIZE_SIZE = 2;
-
- private static final int NO_CHILDREN_ADDRESS = Integer.MIN_VALUE;
- private static final int INVALID_CHARACTER = -1;
-
- private static final int MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT = 0x7F; // 127
- private static final int MAX_CHARGROUPS_IN_A_NODE = 0x7FFF; // 32767
-
- private static final int MAX_TERMINAL_FREQUENCY = 255;
- private static final int MAX_BIGRAM_FREQUENCY = 15;
+ 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
@@ -184,11 +53,78 @@ public class BinaryDictInputOutput {
// 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;
+
+ 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 ((int)mBuffer.get()) & 0xFF;
+ }
+
+ @Override
+ public int readUnsignedShort() {
+ return ((int)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.
*/
- private static class CharEncoding {
+ private static final class CharEncoding {
private static final int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20;
private static final int MAXIMAL_ONE_BYTE_CHARACTER_VALUE = 0xFF;
@@ -196,7 +132,7 @@ public class BinaryDictInputOutput {
/**
* Helper method to find out whether this code fits on one byte
*/
- private static boolean fitsOnOneByte(int character) {
+ private static boolean fitsOnOneByte(final int character) {
return character >= MINIMAL_ONE_BYTE_CHARACTER_VALUE
&& character <= MAXIMAL_ONE_BYTE_CHARACTER_VALUE;
}
@@ -218,10 +154,10 @@ public class BinaryDictInputOutput {
* @param character the character code.
* @return the size in binary encoded-form, either 1 or 3 bytes.
*/
- private static int getCharSize(int character) {
+ private static int getCharSize(final int character) {
// See char encoding in FusionDictionary.java
if (fitsOnOneByte(character)) return 1;
- if (INVALID_CHARACTER == character) return 1;
+ if (FormatSpec.INVALID_CHARACTER == character) return 1;
return 3;
}
@@ -279,7 +215,7 @@ public class BinaryDictInputOutput {
buffer[index++] = (byte)(0xFF & codePoint);
}
}
- buffer[index++] = GROUP_CHARACTERS_TERMINATOR;
+ buffer[index++] = FormatSpec.GROUP_CHARACTERS_TERMINATOR;
return index - origin;
}
@@ -291,7 +227,7 @@ public class BinaryDictInputOutput {
* @param buffer the ByteArrayOutputStream to write to.
* @param word the string to write.
*/
- private static void writeString(ByteArrayOutputStream buffer, final String word) {
+ 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);
@@ -303,37 +239,38 @@ public class BinaryDictInputOutput {
buffer.write((byte) (0xFF & codePoint));
}
}
- buffer.write(GROUP_CHARACTERS_TERMINATOR);
+ buffer.write(FormatSpec.GROUP_CHARACTERS_TERMINATOR);
}
/**
- * Reads a string from a RandomAccessFile. This is the converse of the above method.
+ * Reads a string from a buffer. This is the converse of the above method.
*/
- private static String readString(final RandomAccessFile source) throws IOException {
+ private static String readString(final FusionDictionaryBufferInterface buffer) {
final StringBuilder s = new StringBuilder();
- int character = readChar(source);
- while (character != INVALID_CHARACTER) {
+ int character = readChar(buffer);
+ while (character != FormatSpec.INVALID_CHARACTER) {
s.appendCodePoint(character);
- character = readChar(source);
+ character = readChar(buffer);
}
return s.toString();
}
/**
- * Reads a character from the file.
+ * Reads a character from the buffer.
*
* This follows the character format documented earlier in this source file.
*
- * @param source the file, positioned over an encoded character.
+ * @param buffer the buffer, positioned over an encoded character.
* @return the character code.
*/
- private static int readChar(RandomAccessFile source) throws IOException {
- int character = source.readUnsignedByte();
+ private static int readChar(final FusionDictionaryBufferInterface buffer) {
+ int character = buffer.readUnsignedByte();
if (!fitsOnOneByte(character)) {
- if (GROUP_CHARACTERS_TERMINATOR == character)
- return INVALID_CHARACTER;
+ if (FormatSpec.GROUP_CHARACTERS_TERMINATOR == character) {
+ return FormatSpec.INVALID_CHARACTER;
+ }
character <<= 16;
- character += source.readUnsignedShort();
+ character += buffer.readUnsignedShort();
}
return character;
}
@@ -348,9 +285,9 @@ public class BinaryDictInputOutput {
* @param group the group
* @return the size of the char array, including the terminator if any
*/
- private static int getGroupCharactersSize(CharGroup group) {
+ private static int getGroupCharactersSize(final CharGroup group) {
int size = CharEncoding.getCharArraySize(group.mChars);
- if (group.hasSeveralChars()) size += GROUP_TERMINATOR_SIZE;
+ if (group.hasSeveralChars()) size += FormatSpec.GROUP_TERMINATOR_SIZE;
return size;
}
@@ -359,14 +296,15 @@ public class BinaryDictInputOutput {
* @param count the group count
* @return the size of the group count, either 1 or 2 bytes.
*/
- private static int getGroupCountSize(final int count) {
- if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) {
+ public static int getGroupCountSize(final int count) {
+ if (FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= count) {
return 1;
- } else if (MAX_CHARGROUPS_IN_A_NODE >= count) {
+ } else if (FormatSpec.MAX_CHARGROUPS_IN_A_NODE >= count) {
return 2;
} else {
- throw new RuntimeException("Can't have more than " + MAX_CHARGROUPS_IN_A_NODE
- + " groups in a node (found " + count +")");
+ throw new RuntimeException("Can't have more than "
+ + FormatSpec.MAX_CHARGROUPS_IN_A_NODE + " groups in a node (found " + count
+ + ")");
}
}
@@ -383,14 +321,14 @@ public class BinaryDictInputOutput {
* Compute the size of a shortcut in bytes.
*/
private static int getShortcutSize(final WeightedString shortcut) {
- int size = GROUP_ATTRIBUTE_FLAGS_SIZE;
+ 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 += GROUP_TERMINATOR_SIZE;
+ size += FormatSpec.GROUP_TERMINATOR_SIZE;
return size;
}
@@ -402,7 +340,7 @@ public class BinaryDictInputOutput {
*/
private static int getShortcutListSize(final ArrayList<WeightedString> shortcutList) {
if (null == shortcutList) return 0;
- int size = GROUP_SHORTCUT_LIST_SIZE_SIZE;
+ int size = FormatSpec.GROUP_SHORTCUT_LIST_SIZE_SIZE;
for (final WeightedString shortcut : shortcutList) {
size += getShortcutSize(shortcut);
}
@@ -413,16 +351,18 @@ public class BinaryDictInputOutput {
* 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(CharGroup group) {
- int size = getGroupCharactersSize(group) + GROUP_FLAGS_SIZE;
+ 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 += GROUP_FREQUENCY_SIZE;
- size += GROUP_MAX_ADDRESS_SIZE; // For children address
+ 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 += (GROUP_ATTRIBUTE_FLAGS_SIZE + GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE)
+ size += (FormatSpec.GROUP_ATTRIBUTE_FLAGS_SIZE
+ + FormatSpec.GROUP_ATTRIBUTE_MAX_ADDRESS_SIZE)
* group.mBigrams.size();
}
return size;
@@ -433,25 +373,63 @@ public class BinaryDictInputOutput {
* 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(Node node) {
+ private static void setNodeMaximumSize(final Node node, final FormatOptions options) {
int size = getGroupCountSize(node);
for (CharGroup g : node.mData) {
- final int groupSize = getCharGroupMaximumSize(g);
+ 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.
*/
- private static boolean hasChildrenAddress(int address) {
- return NO_CHILDREN_ADDRESS != 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.FLAG_IS_MOVED) == 1);
}
/**
+ * 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
@@ -461,30 +439,38 @@ public class BinaryDictInputOutput {
* @param address the address
* @return the byte size.
*/
- private static int getByteSize(int address) {
- assert(address < 0x1000000);
+ private static int getByteSize(final int address) {
+ assert(address <= UINT24_MAX);
if (!hasChildrenAddress(address)) {
return 0;
- } else if (Math.abs(address) < 0x100) {
+ } else if (Math.abs(address) <= UINT8_MAX) {
return 1;
- } else if (Math.abs(address) < 0x10000) {
+ } else if (Math.abs(address) <= UINT16_MAX) {
return 2;
} else {
return 3;
}
}
+
+ private static final int SINT8_MAX = 0x7F;
+ private static final int SINT16_MAX = 0x7FFF;
+ private static final int SINT24_MAX = 0x7FFFFF;
+ private static final int MSB8 = 0x80;
+ private static final int MSB16 = 0x8000;
+ 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(Node root) {
+ /* 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(ArrayList<Node> list, Node node) {
+ 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.
@@ -534,9 +520,11 @@ public class BinaryDictInputOutput {
*
* @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(Node node, FusionDictionary dict) {
+ 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) {
@@ -544,26 +532,38 @@ public class BinaryDictInputOutput {
changed = true;
group.mCachedAddress = node.mCachedAddress + size;
}
- int groupSize = GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
- if (group.isTerminal()) groupSize += GROUP_FREQUENCY_SIZE;
- if (null != group.mChildren) {
- final int offsetBasePoint= groupSize + 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;
- groupSize += getByteSize(offset);
+ // 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
- + GROUP_FLAGS_SIZE;
+ + FormatSpec.GROUP_FLAGS_SIZE;
final int addressOfBigram = findAddressOfWord(dict, bigram.mWord);
final int offset = addressOfBigram - offsetBasePoint;
- groupSize += getByteSize(offset) + GROUP_FLAGS_SIZE;
+ 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;
@@ -575,9 +575,11 @@ public class BinaryDictInputOutput {
* 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(ArrayList<Node> flatNodes) {
+ private static int stackNodes(final ArrayList<Node> flatNodes,
+ final FormatOptions formatOptions) {
int nodeOffset = 0;
for (Node n : flatNodes) {
n.mCachedAddress = nodeOffset;
@@ -587,7 +589,10 @@ public class BinaryDictInputOutput {
g.mCachedAddress = groupCountSize + nodeOffset + groupOffset;
groupOffset += g.mCachedSize;
}
- if (groupOffset + groupCountSize != n.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;
@@ -607,13 +612,14 @@ public class BinaryDictInputOutput {
*
* @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(FusionDictionary dict,
- ArrayList<Node> flatNodes) {
+ 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);
- final int offset = stackNodes(flatNodes);
+ 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 + ")");
@@ -624,12 +630,12 @@ public class BinaryDictInputOutput {
changesDone = false;
for (Node n : flatNodes) {
final int oldNodeSize = n.mCachedSize;
- final boolean changed = computeActualNodeSize(n, dict);
+ final boolean changed = computeActualNodeSize(n, dict, formatOptions);
final int newNodeSize = n.mCachedSize;
if (oldNodeSize < newNodeSize) throw new RuntimeException("Increased size ?!");
changesDone |= changed;
}
- stackNodes(flatNodes);
+ stackNodes(flatNodes, formatOptions);
++passes;
if (passes > MAX_PASSES) throw new RuntimeException("Too many passes - probably a bug");
} while (changesDone);
@@ -652,7 +658,7 @@ public class BinaryDictInputOutput {
*
* @param array the array node to check
*/
- private static void checkFlatNodeArray(ArrayList<Node> array) {
+ private static void checkFlatNodeArray(final ArrayList<Node> array) {
int offset = 0;
int index = 0;
for (Node n : array) {
@@ -694,39 +700,70 @@ public class BinaryDictInputOutput {
}
}
+ /**
+ * 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;
+ }
+
private static byte makeCharGroupFlags(final CharGroup group, final int groupAddress,
- final int childrenOffset) {
+ final int childrenOffset, final FormatOptions formatOptions) {
byte flags = 0;
- if (group.mChars.length > 1) flags |= FLAG_HAS_MULTIPLE_CHARS;
+ if (group.mChars.length > 1) flags |= FormatSpec.FLAG_HAS_MULTIPLE_CHARS;
if (group.mFrequency >= 0) {
- flags |= FLAG_IS_TERMINAL;
+ flags |= FormatSpec.FLAG_IS_TERMINAL;
}
if (null != group.mChildren) {
- switch (getByteSize(childrenOffset)) {
- case 1:
- flags |= FLAG_GROUP_ADDRESS_TYPE_ONEBYTE;
- break;
- case 2:
- flags |= FLAG_GROUP_ADDRESS_TYPE_TWOBYTES;
- break;
- case 3:
- flags |= FLAG_GROUP_ADDRESS_TYPE_THREEBYTES;
- break;
- default:
- throw new RuntimeException("Node with a strange address");
- }
+ final int byteSize = formatOptions.mSupportsDynamicUpdate
+ ? FormatSpec.SIGNED_CHILDREN_ADDRESS_SIZE : getByteSize(childrenOffset);
+ switch (byteSize) {
+ 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;
+ default:
+ throw new RuntimeException("Node with a strange address");
+ }
+ } else if (formatOptions.mSupportsDynamicUpdate) {
+ flags |= FormatSpec.FLAG_GROUP_ADDRESS_TYPE_THREEBYTES;
}
if (null != group.mShortcutTargets) {
if (DBG && 0 == group.mShortcutTargets.size()) {
throw new RuntimeException("0-sized shortcut list must be null");
}
- flags |= FLAG_HAS_SHORTCUT_TARGETS;
+ flags |= FormatSpec.FLAG_HAS_SHORTCUT_TARGETS;
}
if (null != group.mBigrams) {
if (DBG && 0 == group.mBigrams.size()) {
throw new RuntimeException("0-sized bigram list must be null");
}
- flags |= FLAG_HAS_BIGRAMS;
+ flags |= FormatSpec.FLAG_HAS_BIGRAMS;
+ }
+ if (group.mIsNotAWord) {
+ flags |= FormatSpec.FLAG_IS_NOT_A_WORD;
+ }
+ if (group.mIsBlacklistEntry) {
+ flags |= FormatSpec.FLAG_IS_BLACKLISTED;
}
return flags;
}
@@ -743,17 +780,17 @@ public class BinaryDictInputOutput {
*/
private static final int makeBigramFlags(final boolean more, final int offset,
int bigramFrequency, final int unigramFrequency, final String word) {
- int bigramFlags = (more ? FLAG_ATTRIBUTE_HAS_NEXT : 0)
- + (offset < 0 ? FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0);
+ int bigramFlags = (more ? FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT : 0)
+ + (offset < 0 ? FormatSpec.FLAG_ATTRIBUTE_OFFSET_NEGATIVE : 0);
switch (getByteSize(offset)) {
case 1:
- bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
+ bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE;
break;
case 2:
- bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
+ bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES;
break;
case 3:
- bigramFlags |= FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
+ bigramFlags |= FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES;
break;
default:
throw new RuntimeException("Strange offset size");
@@ -783,13 +820,14 @@ public class BinaryDictInputOutput {
// 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, and add
- // (discretizedFrequency + 0.5) times this value to get the median value of the step,
- // which is the best approximation. This is how we get the most precise result with
- // only four bits.
- final double stepSize =
- (double)(MAX_TERMINAL_FREQUENCY - unigramFrequency) / (1.5 + MAX_BIGRAM_FREQUENCY);
- final double firstStepStart = 1 + unigramFrequency + (stepSize / 2.0);
+ // 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
@@ -797,19 +835,21 @@ public class BinaryDictInputOutput {
// 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 & FLAG_ATTRIBUTE_FREQUENCY;
+ bigramFlags += finalBigramFrequency & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY;
return bigramFlags;
}
/**
* Makes the 2-byte value for options flags.
*/
- private static final int makeOptionsValue(final FusionDictionary dictionary) {
+ private static final int makeOptionsValue(final FusionDictionary dictionary,
+ final FormatOptions formatOptions) {
final DictionaryOptions options = dictionary.mOptions;
final boolean hasBigrams = dictionary.hasBigrams();
- return (options.mFrenchLigatureProcessing ? FRENCH_LIGATURE_PROCESSING_FLAG : 0)
- + (options.mGermanUmlautProcessing ? GERMAN_UMLAUT_PROCESSING_FLAG : 0)
- + (hasBigrams ? CONTAINS_BIGRAMS_FLAG : 0);
+ 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);
}
/**
@@ -820,7 +860,27 @@ public class BinaryDictInputOutput {
* @return the flags
*/
private static final int makeShortcutFlags(final boolean more, final int frequency) {
- return (more ? FLAG_ATTRIBUTE_HAS_NEXT : 0) + (frequency & FLAG_ATTRIBUTE_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;
+ }
}
/**
@@ -832,13 +892,16 @@ public class BinaryDictInputOutput {
* @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.
*/
- private static int writePlacedNode(FusionDictionary dict, byte[] buffer, Node node) {
+ private static int writePlacedNode(final FusionDictionary dict, byte[] buffer,
+ final Node node, final FormatOptions formatOptions) {
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) {
@@ -855,33 +918,50 @@ public class BinaryDictInputOutput {
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 += GROUP_FLAGS_SIZE + getGroupCharactersSize(group);
+ groupAddress += getGroupHeaderSize(group, formatOptions);
// Sanity checks.
- if (DBG && group.mFrequency > MAX_TERMINAL_FREQUENCY) {
- throw new RuntimeException("A node has a frequency > " + MAX_TERMINAL_FREQUENCY
+ 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 += GROUP_FREQUENCY_SIZE;
+ if (group.mFrequency >= 0) groupAddress += FormatSpec.GROUP_FREQUENCY_SIZE;
final int childrenOffset = null == group.mChildren
- ? NO_CHILDREN_ADDRESS : group.mChildren.mCachedAddress - groupAddress;
- byte flags = makeCharGroupFlags(group, groupAddress, childrenOffset);
+ ? 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++] = GROUP_CHARACTERS_TERMINATOR;
+ buffer[index++] = FormatSpec.GROUP_CHARACTERS_TERMINATOR;
}
if (group.mFrequency >= 0) {
buffer[index++] = (byte) group.mFrequency;
}
- final int shift = writeVariableAddress(buffer, index, childrenOffset);
+
+ 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 += GROUP_SHORTCUT_LIST_SIZE_SIZE;
- groupAddress += GROUP_SHORTCUT_LIST_SIZE_SIZE;
+ 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();
@@ -921,6 +1001,11 @@ public class BinaryDictInputOutput {
}
}
+ 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 "
@@ -990,10 +1075,10 @@ public class BinaryDictInputOutput {
*
* @param destination the stream to write the binary data to.
* @param dict the dictionary to write.
- * @param version the version of the format to write, currently either 1 or 2.
+ * @param formatOptions file format options.
*/
public static void writeDictionaryBinary(final OutputStream destination,
- final FusionDictionary dict, final int version)
+ 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
@@ -1002,36 +1087,39 @@ public class BinaryDictInputOutput {
// 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.
- if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION) {
+ 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 "
- + MINIMUM_SUPPORTED_VERSION + " through " + MAXIMUM_SUPPORTED_VERSION);
+ + FormatSpec.MINIMUM_SUPPORTED_VERSION + " through "
+ + FormatSpec.MAXIMUM_SUPPORTED_VERSION);
}
ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(256);
// The magic number in big-endian order.
- if (version >= FIRST_VERSION_WITH_HEADER_SIZE) {
+ if (version >= FormatSpec.FIRST_VERSION_WITH_HEADER_SIZE) {
// Magic number for version 2+.
- headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 24)));
- headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 16)));
- headerBuffer.write((byte) (0xFF & (VERSION_2_MAGIC_NUMBER >> 8)));
- headerBuffer.write((byte) (0xFF & VERSION_2_MAGIC_NUMBER));
+ 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 & (VERSION_1_MAGIC_NUMBER >> 8)));
- headerBuffer.write((byte) (0xFF & VERSION_1_MAGIC_NUMBER));
+ 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);
+ final int options = makeOptionsValue(dict, formatOptions);
headerBuffer.write((byte) (0xFF & (options >> 8)));
headerBuffer.write((byte) (0xFF & options));
- if (version >= FIRST_VERSION_WITH_HEADER_SIZE) {
+ 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) {
@@ -1062,20 +1150,20 @@ public class BinaryDictInputOutput {
ArrayList<Node> flatNodes = flattenTree(dict.mRoot);
MakedictLog.i("Computing addresses...");
- computeAddresses(dict, flatNodes);
+ 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 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);
+ dataEndOffset = writePlacedNode(dict, buffer, n, formatOptions);
}
if (DBG) showStatistics(flatNodes);
@@ -1090,113 +1178,161 @@ public class BinaryDictInputOutput {
// Input methods: Read a binary dictionary to memory.
// readDictionaryBinary is the public entry point for them.
- static final int[] characterBuffer = new int[MAX_WORD_LENGTH];
- private static CharGroupInfo readCharGroup(RandomAccessFile source,
- final int originalGroupAddress) throws IOException {
+ private 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;
+ }
+ }
+
+ private 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;
+ }
+ }
+
+ private 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 = source.readUnsignedByte();
+ final int flags = buffer.readUnsignedByte();
++addressPointer;
+
+ final int parentAddress = readParentAddress(buffer, options);
+ if (supportsDynamicUpdate(options)) {
+ addressPointer += 3;
+ }
+
final int characters[];
- if (0 != (flags & FLAG_HAS_MULTIPLE_CHARS)) {
+ if (0 != (flags & FormatSpec.FLAG_HAS_MULTIPLE_CHARS)) {
int index = 0;
- int character = CharEncoding.readChar(source);
+ int character = CharEncoding.readChar(buffer);
addressPointer += CharEncoding.getCharSize(character);
while (-1 != character) {
- characterBuffer[index++] = character;
- character = CharEncoding.readChar(source);
+ // 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(characterBuffer, 0, index);
+ characters = Arrays.copyOfRange(CHARACTER_BUFFER, 0, index);
} else {
- final int character = CharEncoding.readChar(source);
+ final int character = CharEncoding.readChar(buffer);
addressPointer += CharEncoding.getCharSize(character);
characters = new int[] { character };
}
final int frequency;
- if (0 != (FLAG_IS_TERMINAL & flags)) {
+ if (0 != (FormatSpec.FLAG_IS_TERMINAL & flags)) {
++addressPointer;
- frequency = source.readUnsignedByte();
+ frequency = buffer.readUnsignedByte();
} else {
frequency = CharGroup.NOT_A_TERMINAL;
}
- int childrenAddress = addressPointer;
- switch (flags & MASK_GROUP_ADDRESS_TYPE) {
- case FLAG_GROUP_ADDRESS_TYPE_ONEBYTE:
- childrenAddress += source.readUnsignedByte();
- addressPointer += 1;
- break;
- case FLAG_GROUP_ADDRESS_TYPE_TWOBYTES:
- childrenAddress += source.readUnsignedShort();
- addressPointer += 2;
- break;
- case FLAG_GROUP_ADDRESS_TYPE_THREEBYTES:
- childrenAddress += (source.readUnsignedByte() << 16) + source.readUnsignedShort();
- addressPointer += 3;
- break;
- case FLAG_GROUP_ADDRESS_TYPE_NOADDRESS:
- default:
- childrenAddress = NO_CHILDREN_ADDRESS;
- break;
+ 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 & FLAG_HAS_SHORTCUT_TARGETS)) {
- final long pointerBefore = source.getFilePointer();
+ if (0 != (flags & FormatSpec.FLAG_HAS_SHORTCUT_TARGETS)) {
+ final int pointerBefore = buffer.position();
shortcutTargets = new ArrayList<WeightedString>();
- source.readUnsignedShort(); // Skip the size
+ buffer.readUnsignedShort(); // Skip the size
while (true) {
- final int targetFlags = source.readUnsignedByte();
- final String word = CharEncoding.readString(source);
+ final int targetFlags = buffer.readUnsignedByte();
+ final String word = CharEncoding.readString(buffer);
shortcutTargets.add(new WeightedString(word,
- targetFlags & FLAG_ATTRIBUTE_FREQUENCY));
- if (0 == (targetFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break;
+ targetFlags & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY));
+ if (0 == (targetFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT)) break;
}
- addressPointer += (source.getFilePointer() - pointerBefore);
+ addressPointer += buffer.position() - pointerBefore;
}
ArrayList<PendingAttribute> bigrams = null;
- if (0 != (flags & FLAG_HAS_BIGRAMS)) {
+ if (0 != (flags & FormatSpec.FLAG_HAS_BIGRAMS)) {
bigrams = new ArrayList<PendingAttribute>();
while (true) {
- final int bigramFlags = source.readUnsignedByte();
+ final int bigramFlags = buffer.readUnsignedByte();
++addressPointer;
- final int sign = 0 == (bigramFlags & FLAG_ATTRIBUTE_OFFSET_NEGATIVE) ? 1 : -1;
+ final int sign = 0 == (bigramFlags & FormatSpec.FLAG_ATTRIBUTE_OFFSET_NEGATIVE)
+ ? 1 : -1;
int bigramAddress = addressPointer;
- switch (bigramFlags & MASK_ATTRIBUTE_ADDRESS_TYPE) {
- case FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
- bigramAddress += sign * source.readUnsignedByte();
+ switch (bigramFlags & FormatSpec.MASK_ATTRIBUTE_ADDRESS_TYPE) {
+ case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_ONEBYTE:
+ bigramAddress += sign * buffer.readUnsignedByte();
addressPointer += 1;
break;
- case FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
- bigramAddress += sign * source.readUnsignedShort();
+ case FormatSpec.FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES:
+ bigramAddress += sign * buffer.readUnsignedShort();
addressPointer += 2;
break;
- case FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES:
- final int offset = ((source.readUnsignedByte() << 16)
- + source.readUnsignedShort());
+ 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 & FLAG_ATTRIBUTE_FREQUENCY,
+ bigrams.add(new PendingAttribute(bigramFlags & FormatSpec.FLAG_ATTRIBUTE_FREQUENCY,
bigramAddress));
- if (0 == (bigramFlags & FLAG_ATTRIBUTE_HAS_NEXT)) break;
+ if (0 == (bigramFlags & FormatSpec.FLAG_ATTRIBUTE_HAS_NEXT)) break;
}
}
return new CharGroupInfo(originalGroupAddress, addressPointer, flags, characters, frequency,
- childrenAddress, shortcutTargets, bigrams);
+ parentAddress, childrenAddress, shortcutTargets, bigrams);
}
/**
- * Reads and returns the char group count out of a file and forwards the pointer.
+ * Reads and returns the char group count out of a buffer and forwards the pointer.
*/
- private static int readCharGroupCount(RandomAccessFile source) throws IOException {
- final int msb = source.readUnsignedByte();
- if (MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT >= msb) {
+ 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 ((MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8)
- + source.readUnsignedByte();
+ return ((FormatSpec.MAX_CHARGROUPS_FOR_ONE_BYTE_CHARGROUP_COUNT & msb) << 8)
+ + buffer.readUnsignedByte();
}
}
@@ -1204,31 +1340,83 @@ public class BinaryDictInputOutput {
// 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:
- // TODO: perform buffered I/O here and in other places in the code.
private static TreeMap<Integer, String> wordCache = new TreeMap<Integer, String>();
/**
* Finds, as a string, the word at the address passed as an argument.
*
- * @param source the file to read from.
+ * @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, as a string.
- * @throws IOException if the file can't be read.
*/
- private static String getWordAtAddress(final RandomAccessFile source, final long headerSize,
- int address) throws IOException {
+ /* packages for tests */ static String getWordAtAddress(
+ final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
+ final FormatOptions formatOptions) {
final String cachedString = wordCache.get(address);
if (null != cachedString) return cachedString;
- final long originalPointer = source.getFilePointer();
- source.seek(headerSize);
- final int count = readCharGroupCount(source);
+
+ final String 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;
+ }
+
+ private static int[] sGetWordBuffer = new int[FormatSpec.MAX_WORD_LENGTH];
+ private static String getWordAtAddressWithParentAddress(
+ final FusionDictionaryBufferInterface buffer, final int headerSize, final int address,
+ final FormatOptions options) {
+ final StringBuilder builder = new StringBuilder();
+
+ int currentAddress = address;
+ int index = FormatSpec.MAX_WORD_LENGTH - 1;
+ // 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));
+ 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 String(sGetWordBuffer, index + 1, FormatSpec.MAX_WORD_LENGTH - index - 1);
+ }
+
+ private static String 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();
String result = null;
CharGroupInfo last = null;
for (int i = count - 1; i >= 0; --i) {
- CharGroupInfo info = readCharGroup(source, groupOffset);
+ CharGroupInfo info = readCharGroup(buffer, groupOffset, options);
groupOffset = info.mEndAddress;
if (info.mOriginalAddress == address) {
builder.append(new String(info.mCharacters, 0, info.mCharacters.length));
@@ -1239,9 +1427,9 @@ public class BinaryDictInputOutput {
if (info.mChildrenAddress > address) {
if (null == last) continue;
builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
- source.seek(last.mChildrenAddress + headerSize);
+ buffer.position(last.mChildrenAddress + headerSize);
groupOffset = last.mChildrenAddress + 1;
- i = source.readUnsignedByte();
+ i = buffer.readUnsignedByte();
last = null;
continue;
}
@@ -1249,67 +1437,91 @@ public class BinaryDictInputOutput {
}
if (0 == i && hasChildrenAddress(last.mChildrenAddress)) {
builder.append(new String(last.mCharacters, 0, last.mCharacters.length));
- source.seek(last.mChildrenAddress + headerSize);
+ buffer.position(last.mChildrenAddress + headerSize);
groupOffset = last.mChildrenAddress + 1;
- i = source.readUnsignedByte();
+ i = buffer.readUnsignedByte();
last = null;
continue;
}
}
- source.seek(originalPointer);
- wordCache.put(address, result);
return result;
}
/**
- * Reads a single node from a binary file.
+ * Reads a single node from a buffer.
*
- * This methods reads the file at the current position of its file pointer. A node is
- * fully expected to start at the current position.
+ * 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 source the data file, correctly positioned at the start of a node.
+ * @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(RandomAccessFile source, long headerSize,
- Map<Integer, Node> reverseNodeMap, Map<Integer, CharGroup> reverseGroupMap)
+ 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 int nodeOrigin = (int)(source.getFilePointer() - headerSize);
- final int count = readCharGroupCount(source);
final ArrayList<CharGroup> nodeContents = new ArrayList<CharGroup>();
- int groupOffset = nodeOrigin + getGroupCountSize(count);
- for (int i = count; i > 0; --i) {
- CharGroupInfo info = readCharGroup(source, groupOffset);
- ArrayList<WeightedString> shortcutTargets = info.mShortcutTargets;
- ArrayList<WeightedString> bigrams = null;
- if (null != info.mBigrams) {
- bigrams = new ArrayList<WeightedString>();
- for (PendingAttribute bigram : info.mBigrams) {
- final String word = getWordAtAddress(source, headerSize, bigram.mAddress);
- bigrams.add(new WeightedString(word, bigram.mFrequency));
+ 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 String word = getWordAtAddress(
+ buffer, headerSize, bigram.mAddress, options);
+ bigrams.add(new WeightedString(word, bigram.mFrequency));
+ }
+ }
+ 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;
}
- if (hasChildrenAddress(info.mChildrenAddress)) {
- Node children = reverseNodeMap.get(info.mChildrenAddress);
- if (null == children) {
- final long currentPosition = source.getFilePointer();
- source.seek(info.mChildrenAddress + headerSize);
- children = readNode(source, headerSize, reverseNodeMap, reverseGroupMap);
- source.seek(currentPosition);
+
+ // 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;
}
- nodeContents.add(
- new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency,
- children));
- } else {
- nodeContents.add(
- new CharGroup(info.mCharacters, shortcutTargets, bigrams, info.mFrequency));
}
- groupOffset = info.mEndAddress;
- }
+ } while (options.mSupportsDynamicUpdate &&
+ buffer.position() != FormatSpec.NO_FORWARD_LINK_ADDRESS);
+
final Node node = new Node(nodeContents);
node.mCachedAddress = nodeOrigin;
reverseNodeMap.put(node.mCachedAddress, node);
@@ -1318,65 +1530,116 @@ public class BinaryDictInputOutput {
/**
* Helper function to get the binary format version from the header.
+ * @throws IOException
*/
- private static int getFormatVersion(final RandomAccessFile source) throws IOException {
- final int magic_v1 = source.readUnsignedShort();
- if (VERSION_1_MAGIC_NUMBER == magic_v1) return source.readUnsignedByte();
- final int magic_v2 = (magic_v1 << 16) + source.readUnsignedShort();
- if (VERSION_2_MAGIC_NUMBER == magic_v2) return source.readUnsignedShort();
- return NOT_A_VERSION_NUMBER;
+ 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;
}
/**
- * Reads a random access file and returns the memory representation of the dictionary.
- *
- * This high-level method takes a binary file and reads its contents, populating a
- * FusionDictionary structure. The optional dict argument is an existing dictionary to
- * which words from the file should be added. If it is null, a new dictionary is created.
- *
- * @param source the file to read.
- * @param dict an optional dictionary to add words to, or null.
- * @return the created (or merged) dictionary.
+ * Helper function to get and validate the binary format version.
+ * @throws UnsupportedFormatException
+ * @throws IOException
*/
- public static FusionDictionary readDictionaryBinary(final RandomAccessFile source,
- final FusionDictionary dict) throws IOException, UnsupportedFormatException {
- // Check file version
- final int version = getFormatVersion(source);
- if (version < MINIMUM_SUPPORTED_VERSION || version > MAXIMUM_SUPPORTED_VERSION ) {
+ 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 "
- + MAXIMUM_SUPPORTED_VERSION);
+ + FormatSpec.MAXIMUM_SUPPORTED_VERSION);
}
+ return version;
+ }
- // Read options
- final int optionsFlags = source.readUnsignedShort();
+ /**
+ * 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 long headerSize;
- final HashMap<String, String> options = new HashMap<String, String>();
- if (version < FIRST_VERSION_WITH_HEADER_SIZE) {
- headerSize = source.getFilePointer();
+ 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 = (source.readUnsignedByte() << 24) + (source.readUnsignedByte() << 16)
- + (source.readUnsignedByte() << 8) + source.readUnsignedByte();
- while (source.getFilePointer() < headerSize) {
- final String key = CharEncoding.readString(source);
- final String value = CharEncoding.readString(source);
- options.put(key, value);
- }
- source.seek(headerSize);
+ 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.
+ */
+ 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(source, headerSize, reverseNodeMapping, reverseGroupMapping);
+ final Node root = readNode(buffer, header.mHeaderSize, reverseNodeMapping,
+ reverseGroupMapping, header.mFormatOptions);
- FusionDictionary newDict = new FusionDictionary(root,
- new FusionDictionary.DictionaryOptions(options,
- 0 != (optionsFlags & GERMAN_UMLAUT_PROCESSING_FLAG),
- 0 != (optionsFlags & FRENCH_LIGATURE_PROCESSING_FLAG)));
+ FusionDictionary newDict = new FusionDictionary(root, header.mDictionaryOptions);
if (null != dict) {
for (final Word w : dict) {
- newDict.add(w.mWord, w.mFrequency, w.mShortcutTargets);
+ 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
@@ -1400,14 +1663,45 @@ public class BinaryDictInputOutput {
* @return true if it's a binary dictionary, false otherwise
*/
public static boolean isBinaryDictionary(final String filename) {
+ FileInputStream inStream = null;
try {
- RandomAccessFile f = new RandomAccessFile(filename, "r");
- final int version = getFormatVersion(f);
- return (version >= MINIMUM_SUPPORTED_VERSION && version <= MAXIMUM_SUPPORTED_VERSION);
+ final File file = new File(filename);
+ 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 = (float)unigramFrequency
+ + stepSize * (bigramFrequency + 1.0f);
+ return (int)resultFreqFloat;
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java b/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
index ef7dbb251..8e64082e6 100644
--- a/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
+++ b/java/src/com/android/inputmethod/latin/makedict/CharGroupInfo.java
@@ -23,7 +23,7 @@ import java.util.ArrayList;
/**
* Raw char group info straight out of a file. This will contain numbers for addresses.
*/
-public class CharGroupInfo {
+public final class CharGroupInfo {
public final int mOriginalAddress;
public final int mEndAddress;
@@ -31,18 +31,20 @@ public class CharGroupInfo {
public final int[] mCharacters;
public final int mFrequency;
public final int mChildrenAddress;
+ public final int mParentAddress;
public final ArrayList<WeightedString> mShortcutTargets;
public final ArrayList<PendingAttribute> mBigrams;
public CharGroupInfo(final int originalAddress, final int endAddress, final int flags,
- final int[] characters, final int frequency, final int childrenAddress,
- final ArrayList<WeightedString> shortcutTargets,
+ final int[] characters, final int frequency, final int parentAddress,
+ final int childrenAddress, final ArrayList<WeightedString> shortcutTargets,
final ArrayList<PendingAttribute> bigrams) {
mOriginalAddress = originalAddress;
mEndAddress = endAddress;
mFlags = flags;
mCharacters = characters;
mFrequency = frequency;
+ mParentAddress = parentAddress;
mChildrenAddress = childrenAddress;
mShortcutTargets = shortcutTargets;
mBigrams = bigrams;
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
new file mode 100644
index 000000000..b3fbb9fb5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin.makedict;
+
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
+
+/**
+ * Dictionary File Format Specification.
+ */
+public final class FormatSpec {
+
+ /*
+ * Array of Node(FusionDictionary.Node) layout is as follows:
+ *
+ * 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
+ *
+ * g |
+ * r | sequence of groups,
+ * o | the layout of each group is described below.
+ * u |
+ * ps
+ *
+ * f |
+ * o | IF SUPPORTS_DYNAMIC_UPDATE (defined in the file header)
+ * r | forward link address, 3byte
+ * w | 1 byte = bbbbbbbb match
+ * a | case 1xxxxxxx => -((xxxxxxx << 16) + (next byte << 8) + next byte)
+ * r | otherwise => (xxxxxxx << 16) + (next byte << 8) + next byte
+ * d |
+ * linkaddress
+ */
+
+ /* Node(CharGroup) 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
+ * g | ELSE
+ * s | is moved ? 2 bits, 11 = no
+ * | 01 = yes
+ * | the new address is stored in the same place as the parent address
+ * | 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
+ * | has bigrams ? 1 bit, 1 = yes, 0 = no : FLAG_HAS_BIGRAMS
+ * | is not a word ? 1 bit, 1 = yes, 0 = no : FLAG_IS_NOT_A_WORD
+ * | is blacklisted ? 1 bit, 1 = yes, 0 = no : FLAG_IS_BLACKLISTED
+ *
+ * p |
+ * a | IF SUPPORTS_DYNAMIC_UPDATE (defined in the file header)
+ * r | parent address, 3byte
+ * e | 1 byte = bbbbbbbb match
+ * n | case 1xxxxxxx => -((0xxxxxxx << 16) + (next byte << 8) + next byte)
+ * t | otherwise => (bbbbbbbb << 16) + (next byte << 8) + next byte
+ * a |
+ * ddress
+ *
+ * c | IF FLAG_HAS_MULTIPLE_CHARS
+ * h | char, char, char, char n * (1 or 3 bytes) : use CharGroupInfo for i/o helpers
+ * a | end 1 byte, = 0
+ * r | ELSE
+ * s | char 1 or 3 bytes
+ * | END
+ *
+ * f |
+ * r | IF FLAG_IS_TERMINAL
+ * 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
+ *
+ * | IF FLAG_IS_TERMINAL && FLAG_HAS_SHORTCUT_TARGETS
+ * | shortcut string list
+ * | IF FLAG_IS_TERMINAL && FLAG_HAS_BIGRAMS
+ * | bigrams address list
+ *
+ * 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).
+ *
+ * bigram address list is:
+ * <flags> = | hasNext = 1 bit, 1 = yes, 0 = no : FLAG_ATTRIBUTE_HAS_NEXT
+ * | addressSign = 1 bit, : FLAG_ATTRIBUTE_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)
+ * | read 1 byte, add top 4 bits
+ * | ELSIF (10 == FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES == addressFormat)
+ * | read 2 bytes, add top 4 bits
+ * | ELSE // 11 == FLAG_ATTRIBUTE_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
+ *
+ * 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
+ * | reserved = 3 bits, must be 0
+ * | 4 bits : frequency : mask with FLAG_ATTRIBUTE_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
+ */
+
+ 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;
+ 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;
+
+ // These options need to be the same numeric values as the one in the native reading code.
+ static final int GERMAN_UMLAUT_PROCESSING_FLAG = 0x1;
+ // TODO: Make the native reading code read this variable.
+ static final int SUPPORTS_DYNAMIC_UPDATE = 0x2;
+ static final int FRENCH_LIGATURE_PROCESSING_FLAG = 0x4;
+ static final int CONTAINS_BIGRAMS_FLAG = 0x8;
+
+ // 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 PARENT_ADDRESS_SIZE = 3;
+ static final int FORWARD_LINK_ADDRESS_SIZE = 3;
+
+ 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 FLAG_HAS_MULTIPLE_CHARS = 0x20;
+
+ static final int FLAG_IS_TERMINAL = 0x10;
+ static final int FLAG_HAS_SHORTCUT_TARGETS = 0x08;
+ static final int FLAG_HAS_BIGRAMS = 0x04;
+ static final int FLAG_IS_NOT_A_WORD = 0x02;
+ static final int FLAG_IS_BLACKLISTED = 0x01;
+ static final int FLAG_IS_MOVED = 0x40;
+
+ 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 GROUP_CHARACTERS_TERMINATOR = 0x1F;
+
+ 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;
+
+ 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_TERMINAL_FREQUENCY = 255;
+ static final int MAX_BIGRAM_FREQUENCY = 15;
+
+ // This option needs to be the same numeric value as the one in binary_format.h.
+ static final int NOT_VALID_WORD = -99;
+ static final int SIGNED_CHILDREN_ADDRESS_SIZE = 3;
+
+ /**
+ * Options about file format.
+ */
+ public static final class FormatOptions {
+ public final int mVersion;
+ public final boolean mSupportsDynamicUpdate;
+ public FormatOptions(final int version) {
+ this(version, false);
+ }
+ public FormatOptions(final int version, final boolean supportsDynamicUpdate) {
+ mVersion = version;
+ if (version < FIRST_VERSION_WITH_DYNAMIC_UPDATE && supportsDynamicUpdate) {
+ throw new RuntimeException("Dynamic updates are only supported with versions "
+ + FIRST_VERSION_WITH_DYNAMIC_UPDATE + " and ulterior.");
+ }
+ mSupportsDynamicUpdate = supportsDynamicUpdate;
+ }
+ }
+
+ /**
+ * Class representing file header.
+ */
+ static final class FileHeader {
+ public final int mHeaderSize;
+ public final DictionaryOptions mDictionaryOptions;
+ public final FormatOptions mFormatOptions;
+ public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions,
+ final FormatOptions formatOptions) {
+ mHeaderSize = headerSize;
+ mDictionaryOptions = dictionaryOptions;
+ mFormatOptions = formatOptions;
+ }
+ }
+
+ 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 8b53c9427..3193ef457 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -16,6 +16,8 @@
package com.android.inputmethod.latin.makedict;
+import com.android.inputmethod.latin.Constants;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -26,8 +28,7 @@ import java.util.LinkedList;
/**
* A dictionary that can fusion heads and tails of words for more compression.
*/
-public class FusionDictionary implements Iterable<Word> {
-
+public final class FusionDictionary implements Iterable<Word> {
private static final boolean DBG = MakedictLog.DBG;
/**
@@ -38,20 +39,18 @@ public class FusionDictionary implements Iterable<Word> {
* This class also contains fields to cache size and address, to help with binary
* generation.
*/
- public static class Node {
+ public static final class Node {
ArrayList<CharGroup> mData;
// To help with binary generation
- int mCachedSize;
- int mCachedAddress;
+ int mCachedSize = Integer.MIN_VALUE;
+ int mCachedAddress = Integer.MIN_VALUE;
+ int mCachedParentAddress = 0;
+
public Node() {
mData = new ArrayList<CharGroup>();
- mCachedSize = Integer.MIN_VALUE;
- mCachedAddress = Integer.MIN_VALUE;
}
public Node(ArrayList<CharGroup> data) {
mData = data;
- mCachedSize = Integer.MIN_VALUE;
- mCachedAddress = Integer.MIN_VALUE;
}
}
@@ -60,9 +59,9 @@ public class FusionDictionary implements Iterable<Word> {
*
* This represents an "attribute", that is either a bigram or a shortcut.
*/
- public static class WeightedString {
- final String mWord;
- int mFrequency;
+ public static final class WeightedString {
+ public final String mWord;
+ public int mFrequency;
public WeightedString(String word, int frequency) {
mWord = word;
mFrequency = frequency;
@@ -94,33 +93,41 @@ public class FusionDictionary implements Iterable<Word> {
* 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 class CharGroup {
+ public static final class CharGroup {
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;
+ 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,
- final ArrayList<WeightedString> bigrams, final int frequency) {
+ final ArrayList<WeightedString> bigrams, final int frequency,
+ final boolean isNotAWord, final boolean isBlacklistEntry) {
mChars = chars;
mFrequency = frequency;
mShortcutTargets = shortcutTargets;
mBigrams = bigrams;
mChildren = null;
+ mIsNotAWord = isNotAWord;
+ mIsBlacklistEntry = isBlacklistEntry;
}
public CharGroup(final int[] chars, final ArrayList<WeightedString> shortcutTargets,
- final ArrayList<WeightedString> bigrams, final int frequency, final Node children) {
+ final ArrayList<WeightedString> bigrams, final int frequency,
+ final boolean isNotAWord, final boolean isBlacklistEntry, final Node children) {
mChars = chars;
mFrequency = frequency;
mShortcutTargets = shortcutTargets;
mBigrams = bigrams;
mChildren = children;
+ mIsNotAWord = isNotAWord;
+ mIsBlacklistEntry = isBlacklistEntry;
}
public void addChild(CharGroup n) {
@@ -197,8 +204,9 @@ public class FusionDictionary implements Iterable<Word> {
* the existing ones if any. Note: unigram, bigram, and shortcut frequencies are only
* updated if they are higher than the existing ones.
*/
- public void update(int frequency, ArrayList<WeightedString> shortcutTargets,
- ArrayList<WeightedString> bigrams) {
+ public void update(final int frequency, final ArrayList<WeightedString> shortcutTargets,
+ final ArrayList<WeightedString> bigrams,
+ final boolean isNotAWord, final boolean isBlacklistEntry) {
if (frequency > mFrequency) {
mFrequency = frequency;
}
@@ -234,6 +242,8 @@ public class FusionDictionary implements Iterable<Word> {
}
}
}
+ mIsNotAWord = isNotAWord;
+ mIsBlacklistEntry = isBlacklistEntry;
}
}
@@ -242,7 +252,7 @@ public class FusionDictionary implements Iterable<Word> {
*
* There are no options at the moment, so this class is empty.
*/
- public static class DictionaryOptions {
+ public static final class DictionaryOptions {
public final boolean mGermanUmlautProcessing;
public final boolean mFrenchLigatureProcessing;
public final HashMap<String, String> mAttributes;
@@ -296,10 +306,24 @@ public class FusionDictionary implements Iterable<Word> {
* @param word the word to add.
* @param frequency the frequency of the word, in the range [0..255].
* @param shortcutTargets a list of shortcut targets for this word, or null.
+ * @param isNotAWord true if this should not be considered a word (e.g. shortcut only)
*/
public void add(final String word, final int frequency,
- final ArrayList<WeightedString> shortcutTargets) {
- add(getCodePoints(word), frequency, shortcutTargets);
+ final ArrayList<WeightedString> shortcutTargets, final boolean isNotAWord) {
+ add(getCodePoints(word), frequency, shortcutTargets, isNotAWord,
+ false /* isBlacklistEntry */);
+ }
+
+ /**
+ * Helper method to add a blacklist entry as a string.
+ *
+ * @param word the word to add as a blacklist entry.
+ * @param shortcutTargets a list of shortcut targets for this word, or null.
+ * @param isNotAWord true if this is not a word for spellcheking purposes (shortcut only or so)
+ */
+ public void addBlacklistEntry(final String word,
+ final ArrayList<WeightedString> shortcutTargets, final boolean isNotAWord) {
+ add(getCodePoints(word), 0, shortcutTargets, isNotAWord, true /* isBlacklistEntry */);
}
/**
@@ -332,7 +356,8 @@ public class FusionDictionary implements Iterable<Word> {
if (charGroup != null) {
final CharGroup charGroup2 = findWordInTree(mRoot, word2);
if (charGroup2 == null) {
- add(getCodePoints(word2), 0, null);
+ add(getCodePoints(word2), 0, null, false /* isNotAWord */,
+ false /* isBlacklistEntry */);
}
charGroup.addBigram(word2, frequency);
} else {
@@ -349,10 +374,18 @@ public class FusionDictionary implements Iterable<Word> {
* @param word the word, as an int array.
* @param frequency the frequency of the word, in the range [0..255].
* @param shortcutTargets an optional list of shortcut targets for this word (null if none).
+ * @param isNotAWord true if this is not a word for spellcheking purposes (shortcut only or so)
+ * @param isBlacklistEntry true if this is a blacklisted word, false otherwise
*/
private void add(final int[] word, final int frequency,
- final ArrayList<WeightedString> shortcutTargets) {
+ final ArrayList<WeightedString> shortcutTargets,
+ final boolean isNotAWord, final boolean isBlacklistEntry) {
assert(frequency >= 0 && frequency <= 255);
+ if (word.length >= Constants.Dictionary.MAX_WORD_LENGTH) {
+ MakedictLog.w("Ignoring a word that is too long: word.length = " + word.length);
+ return;
+ }
+
Node currentNode = mRoot;
int charIndex = 0;
@@ -376,7 +409,7 @@ public class FusionDictionary implements Iterable<Word> {
final int insertionIndex = findInsertionIndex(currentNode, word[charIndex]);
final CharGroup newGroup = new CharGroup(
Arrays.copyOfRange(word, charIndex, word.length),
- shortcutTargets, null /* bigrams */, frequency);
+ shortcutTargets, null /* bigrams */, frequency, isNotAWord, isBlacklistEntry);
currentNode.mData.add(insertionIndex, newGroup);
if (DBG) checkStack(currentNode);
} else {
@@ -386,13 +419,15 @@ public class FusionDictionary implements Iterable<Word> {
// 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,
// make it one by filling in its frequency and other attributes
- currentGroup.update(frequency, shortcutTargets, null);
+ currentGroup.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(
Arrays.copyOfRange(word, charIndex + differentCharIndex, word.length),
- shortcutTargets, null /* bigrams */, frequency);
+ shortcutTargets, null /* bigrams */, frequency, isNotAWord,
+ isBlacklistEntry);
currentGroup.mChildren = new Node();
currentGroup.mChildren.mData.add(newNode);
}
@@ -400,7 +435,9 @@ public class FusionDictionary implements Iterable<Word> {
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.update(frequency, shortcutTargets, null,
+ currentGroup.mIsNotAWord && isNotAWord,
+ currentGroup.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.
@@ -408,21 +445,26 @@ public class FusionDictionary implements Iterable<Word> {
final CharGroup newOldWord = new CharGroup(
Arrays.copyOfRange(currentGroup.mChars, differentCharIndex,
currentGroup.mChars.length), currentGroup.mShortcutTargets,
- currentGroup.mBigrams, currentGroup.mFrequency, currentGroup.mChildren);
+ currentGroup.mBigrams, currentGroup.mFrequency,
+ currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry,
+ currentGroup.mChildren);
newChildren.mData.add(newOldWord);
final CharGroup newParent;
if (charIndex + differentCharIndex >= word.length) {
newParent = new CharGroup(
Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
- shortcutTargets, null /* bigrams */, frequency, newChildren);
+ shortcutTargets, null /* bigrams */, frequency,
+ isNotAWord, isBlacklistEntry, newChildren);
} else {
newParent = new CharGroup(
Arrays.copyOfRange(currentGroup.mChars, 0, differentCharIndex),
- null /* shortcutTargets */, null /* bigrams */, -1, newChildren);
+ null /* shortcutTargets */, null /* bigrams */, -1,
+ false /* isNotAWord */, false /* isBlacklistEntry */, newChildren);
final CharGroup newWord = new CharGroup(Arrays.copyOfRange(word,
charIndex + differentCharIndex, word.length),
- shortcutTargets, null /* bigrams */, frequency);
+ shortcutTargets, null /* bigrams */, frequency,
+ isNotAWord, isBlacklistEntry);
final int addIndex = word[charIndex + differentCharIndex]
> currentGroup.mChars[differentCharIndex] ? 1 : 0;
newChildren.mData.add(addIndex, newWord);
@@ -468,7 +510,7 @@ public class FusionDictionary implements Iterable<Word> {
* is ignored.
* This comparator imposes orderings that are inconsistent with equals.
*/
- static private class CharGroupComparator implements java.util.Comparator<CharGroup> {
+ static private final class CharGroupComparator implements java.util.Comparator<CharGroup> {
@Override
public int compare(CharGroup c1, CharGroup c2) {
if (c1.mChars[0] == c2.mChars[0]) return 0;
@@ -483,7 +525,8 @@ public class FusionDictionary implements Iterable<Word> {
private static int findInsertionIndex(final Node node, int character) {
final ArrayList<CharGroup> data = node.mData;
final CharGroup reference = new CharGroup(new int[] { character },
- null /* shortcutTargets */, null /* bigrams */, 0);
+ null /* shortcutTargets */, null /* bigrams */, 0, false /* isNotAWord */,
+ false /* isBlacklistEntry */);
int result = Collections.binarySearch(data, reference, CHARGROUP_COMPARATOR);
return result >= 0 ? result : -result - 1;
}
@@ -512,17 +555,28 @@ public class FusionDictionary implements Iterable<Word> {
final StringBuilder checker = DBG ? new StringBuilder() : null;
CharGroup currentGroup;
+ final int codePointCountInS = s.codePointCount(0, s.length());
do {
int indexOfGroup = findIndexOfChar(node, s.codePointAt(index));
if (CHARACTER_NOT_FOUND == indexOfGroup) return null;
currentGroup = node.mData.get(indexOfGroup);
+
+ if (s.length() - index < currentGroup.mChars.length) return null;
+ int newIndex = index;
+ while (newIndex < s.length() && newIndex - index < currentGroup.mChars.length) {
+ if (currentGroup.mChars[newIndex - index] != s.codePointAt(newIndex)) return null;
+ newIndex++;
+ }
+ index = newIndex;
+
if (DBG) checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length));
- index += currentGroup.mChars.length;
- if (index < s.length()) {
+ if (index < codePointCountInS) {
node = currentGroup.mChildren;
}
- } while (null != node && index < s.length());
+ } while (null != node && index < codePointCountInS);
+ if (index < codePointCountInS) return null;
+ if (!currentGroup.isTerminal()) return null;
if (DBG && !s.equals(checker.toString())) return null;
return currentGroup;
}
@@ -679,7 +733,7 @@ public class FusionDictionary implements Iterable<Word> {
// StringBuilder s = new StringBuilder();
// for (CharGroup g : node.data) {
// s.append(g.frequency);
-// for (int ch : g.chars){
+// for (int ch : g.chars) {
// s.append(Character.toChars(ch));
// }
// }
@@ -691,9 +745,8 @@ public class FusionDictionary implements Iterable<Word> {
*
* This is purely for convenience.
*/
- public static class DictionaryIterator implements Iterator<Word> {
-
- private static class Position {
+ public static final class DictionaryIterator implements Iterator<Word> {
+ private static final class Position {
public Iterator<CharGroup> pos;
public int length;
public Position(ArrayList<CharGroup> groups) {
@@ -738,13 +791,14 @@ public class FusionDictionary implements Iterable<Word> {
}
if (currentGroup.mFrequency >= 0)
return new Word(mCurrentString.toString(), currentGroup.mFrequency,
- currentGroup.mShortcutTargets, currentGroup.mBigrams);
+ currentGroup.mShortcutTargets, currentGroup.mBigrams,
+ currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry);
} else {
mPositions.removeLast();
currentPos = mPositions.getLast();
mCurrentString.setLength(mCurrentString.length() - mPositions.getLast().length);
}
- } while(true);
+ } while (true);
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java b/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java
index 3f0cd0796..6c6b00b6a 100644
--- a/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java
+++ b/java/src/com/android/inputmethod/latin/makedict/MakedictLog.java
@@ -21,7 +21,7 @@ import android.util.Log;
/**
* Wrapper to redirect log events to the right output medium.
*/
-public class MakedictLog {
+public final class MakedictLog {
public static final boolean DBG = false;
private static final String TAG = MakedictLog.class.getSimpleName();
diff --git a/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java b/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java
index 5b41d27f2..5bb24da74 100644
--- a/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java
+++ b/java/src/com/android/inputmethod/latin/makedict/PendingAttribute.java
@@ -22,7 +22,7 @@ package com.android.inputmethod.latin.makedict;
* An attribute is either a bigram or a shortcut.
* All instances of this class are always immutable.
*/
-public class PendingAttribute {
+public final class PendingAttribute {
public final int mFrequency;
public final int mAddress;
public PendingAttribute(final int frequency, final int address) {
diff --git a/java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java b/java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java
index bd42fb8fa..dbb2ea870 100644
--- a/java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java
+++ b/java/src/com/android/inputmethod/latin/makedict/UnsupportedFormatException.java
@@ -19,7 +19,7 @@ package com.android.inputmethod.latin.makedict;
/**
* Simple exception thrown when a file format is not recognized.
*/
-public class UnsupportedFormatException extends Exception {
+public final class UnsupportedFormatException extends Exception {
public UnsupportedFormatException(String description) {
super(description);
}
diff --git a/java/src/com/android/inputmethod/latin/makedict/Word.java b/java/src/com/android/inputmethod/latin/makedict/Word.java
index d07826757..4c4f18f1a 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Word.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Word.java
@@ -26,21 +26,26 @@ import java.util.Arrays;
*
* This is chiefly used to iterate a dictionary.
*/
-public class Word implements Comparable<Word> {
- final String mWord;
- final int mFrequency;
- final ArrayList<WeightedString> mShortcutTargets;
- final ArrayList<WeightedString> mBigrams;
+public final class Word implements Comparable<Word> {
+ public final String mWord;
+ public final int mFrequency;
+ public final ArrayList<WeightedString> mShortcutTargets;
+ public final ArrayList<WeightedString> mBigrams;
+ public final boolean mIsNotAWord;
+ public final boolean mIsBlacklistEntry;
private int mHashCode = 0;
public Word(final String word, final int frequency,
final ArrayList<WeightedString> shortcutTargets,
- final ArrayList<WeightedString> bigrams) {
+ final ArrayList<WeightedString> bigrams,
+ final boolean isNotAWord, final boolean isBlacklistEntry) {
mWord = word;
mFrequency = frequency;
mShortcutTargets = shortcutTargets;
mBigrams = bigrams;
+ mIsNotAWord = isNotAWord;
+ mIsBlacklistEntry = isBlacklistEntry;
}
private static int computeHashCode(Word word) {
@@ -48,7 +53,9 @@ public class Word implements Comparable<Word> {
word.mWord,
word.mFrequency,
word.mShortcutTargets.hashCode(),
- word.mBigrams.hashCode()
+ word.mBigrams.hashCode(),
+ word.mIsNotAWord,
+ word.mIsBlacklistEntry
});
}
@@ -78,7 +85,9 @@ public class Word implements Comparable<Word> {
Word w = (Word)o;
return mFrequency == w.mFrequency && mWord.equals(w.mWord)
&& mShortcutTargets.equals(w.mShortcutTargets)
- && mBigrams.equals(w.mBigrams);
+ && mBigrams.equals(w.mBigrams)
+ && mIsNotAWord == w.mIsNotAWord
+ && mIsBlacklistEntry == w.mIsBlacklistEntry;
}
@Override
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index ba974ff7f..5a11ae534 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -16,37 +16,26 @@
package com.android.inputmethod.latin.spellcheck;
-import android.content.ContentResolver;
import android.content.Intent;
import android.content.SharedPreferences;
-import android.database.ContentObserver;
import android.preference.PreferenceManager;
-import android.provider.UserDictionary.Words;
import android.service.textservice.SpellCheckerService;
-import android.text.TextUtils;
import android.util.Log;
-import android.util.LruCache;
-import android.view.textservice.SentenceSuggestionsInfo;
import android.view.textservice.SuggestionsInfo;
-import android.view.textservice.TextInfo;
-import com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
import com.android.inputmethod.keyboard.ProximityInfo;
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.Dictionary.WordCallback;
import com.android.inputmethod.latin.DictionaryCollection;
import com.android.inputmethod.latin.DictionaryFactory;
-import com.android.inputmethod.latin.LatinIME;
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.SynchronouslyLoadedContactsDictionary;
import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary;
-import com.android.inputmethod.latin.SynchronouslyLoadedUserDictionary;
-import com.android.inputmethod.latin.WhitelistDictionary;
-import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.UserBinaryDictionary;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@@ -61,7 +50,7 @@ import java.util.TreeMap;
/**
* Service for spell checking, using LatinIME's dictionaries and mechanisms.
*/
-public class AndroidSpellCheckerService extends SpellCheckerService
+public final class AndroidSpellCheckerService extends SpellCheckerService
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
private static final boolean DBG = false;
@@ -69,18 +58,15 @@ public class AndroidSpellCheckerService extends SpellCheckerService
public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
- private static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
- private static final int CAPITALIZE_FIRST = 1; // First only
- private static final int CAPITALIZE_ALL = 2; // All caps
+ 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
private final static String[] EMPTY_STRING_ARRAY = new String[0];
- private Map<String, DictionaryPool> mDictionaryPools =
- Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
- private Map<String, Dictionary> mUserDictionaries =
- Collections.synchronizedMap(new TreeMap<String, Dictionary>());
- private Map<String, Dictionary> mWhitelistDictionaries =
- Collections.synchronizedMap(new TreeMap<String, Dictionary>());
- private Dictionary mContactsDictionary;
+ private Map<String, DictionaryPool> mDictionaryPools = CollectionUtils.newSynchronizedTreeMap();
+ private Map<String, UserBinaryDictionary> mUserDictionaries =
+ CollectionUtils.newSynchronizedTreeMap();
+ private ContactsBinaryDictionary mContactsDictionary;
// The threshold for a candidate to be offered as a suggestion.
private float mSuggestionThreshold;
@@ -91,12 +77,12 @@ public class AndroidSpellCheckerService extends SpellCheckerService
private final Object mUseContactsLock = new Object();
private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList =
- new HashSet<WeakReference<DictionaryCollection>>();
+ CollectionUtils.newHashSet();
public static final int SCRIPT_LATIN = 0;
public static final int SCRIPT_CYRILLIC = 1;
- private static final String SINGLE_QUOTE = "\u0027";
- private static final String APOSTROPHE = "\u2019";
+ public static final String SINGLE_QUOTE = "\u0027";
+ public static final String APOSTROPHE = "\u2019";
private static final TreeMap<String, Integer> mLanguageToScript;
static {
// List of the supported languages and their associated script. We won't check
@@ -107,7 +93,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService
// proximity to pass to the dictionary descent algorithm.
// IMPORTANT: this only contains languages - do not write countries in there.
// Only the language is searched from the map.
- mLanguageToScript = new TreeMap<String, Integer>();
+ mLanguageToScript = CollectionUtils.newTreeMap();
mLanguageToScript.put("en", SCRIPT_LATIN);
mLanguageToScript.put("fr", SCRIPT_LATIN);
mLanguageToScript.put("de", SCRIPT_LATIN);
@@ -133,7 +119,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService
onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY);
}
- private static int getScriptFromLocale(final Locale locale) {
+ public static int getScriptFromLocale(final Locale locale) {
final Integer script = mLanguageToScript.get(locale.getLanguage());
if (null == script) {
throw new RuntimeException("We have been called with an unsupported language: \""
@@ -157,13 +143,9 @@ public class AndroidSpellCheckerService extends SpellCheckerService
private void startUsingContactsDictionaryLocked() {
if (null == mContactsDictionary) {
- if (LatinIME.USE_BINARY_CONTACTS_DICTIONARY) {
- // TODO: use the right locale for each session
- mContactsDictionary =
- new SynchronouslyLoadedContactsBinaryDictionary(this, Locale.getDefault());
- } else {
- mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
- }
+ // TODO: use the right locale for each session
+ mContactsDictionary =
+ new SynchronouslyLoadedContactsBinaryDictionary(this, Locale.getDefault());
}
final Iterator<WeakReference<DictionaryCollection>> iterator =
mDictionaryCollectionsList.iterator();
@@ -199,20 +181,28 @@ public class AndroidSpellCheckerService extends SpellCheckerService
@Override
public Session createSession() {
- return new AndroidSpellCheckerSession(this);
+ // Should not refer to AndroidSpellCheckerSession directly considering
+ // that AndroidSpellCheckerSession may be overlaid.
+ return AndroidSpellCheckerSessionFactory.newInstance(this);
}
- private static SuggestionsInfo getNotInDictEmptySuggestions() {
+ public static SuggestionsInfo getNotInDictEmptySuggestions() {
return new SuggestionsInfo(0, EMPTY_STRING_ARRAY);
}
- private static SuggestionsInfo getInDictEmptySuggestions() {
+ public static SuggestionsInfo getInDictEmptySuggestions() {
return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY,
EMPTY_STRING_ARRAY);
}
- private static class SuggestionsGatherer implements WordCallback {
- public static class Result {
+ public SuggestionsGatherer newSuggestionsGatherer(final String text, int maxLength) {
+ return new SuggestionsGatherer(
+ text, mSuggestionThreshold, mRecommendedThreshold, maxLength);
+ }
+
+ // TODO: remove this class and replace it by storage local to the session.
+ public static final class SuggestionsGatherer {
+ public static final class Result {
public final String[] mSuggestions;
public final boolean mHasRecommendedSuggestions;
public Result(final String[] gatheredSuggestions,
@@ -241,13 +231,12 @@ public class AndroidSpellCheckerService extends SpellCheckerService
mSuggestionThreshold = suggestionThreshold;
mRecommendedThreshold = recommendedThreshold;
mMaxLength = maxLength;
- mSuggestions = new ArrayList<CharSequence>(maxLength + 1);
+ mSuggestions = CollectionUtils.newArrayList(maxLength + 1);
mScores = new int[mMaxLength];
}
- @Override
- synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
- int dicTypeId, int dataType) {
+ synchronized public boolean addWord(char[] word, int[] spaceIndices, int wordOffset,
+ int wordLength, int score) {
final int positionIndex = Arrays.binarySearch(mScores, 0, mLength, score);
// binarySearch returns the index if the element exists, and -<insertion index> - 1
// if it doesn't. See documentation for binarySearch.
@@ -370,11 +359,9 @@ public class AndroidSpellCheckerService extends SpellCheckerService
private void closeAllDictionaries() {
final Map<String, DictionaryPool> oldPools = mDictionaryPools;
- mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
- final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries;
- mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
- final Map<String, Dictionary> oldWhitelistDictionaries = mWhitelistDictionaries;
- mWhitelistDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
+ mDictionaryPools = CollectionUtils.newSynchronizedTreeMap();
+ final Map<String, UserBinaryDictionary> oldUserDictionaries = mUserDictionaries;
+ mUserDictionaries = CollectionUtils.newSynchronizedTreeMap();
new Thread("spellchecker_close_dicts") {
@Override
public void run() {
@@ -384,15 +371,12 @@ public class AndroidSpellCheckerService extends SpellCheckerService
for (Dictionary dict : oldUserDictionaries.values()) {
dict.close();
}
- for (Dictionary dict : oldWhitelistDictionaries.values()) {
- dict.close();
- }
synchronized (mUseContactsLock) {
if (null != mContactsDictionary) {
// The synchronously loaded contacts dictionary should have been in one
// or several pools, but it is shielded against multiple closing and it's
// safe to call it several times.
- final Dictionary dictToClose = mContactsDictionary;
+ final ContactsBinaryDictionary dictToClose = mContactsDictionary;
// TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY
// is no longer needed
mContactsDictionary = null;
@@ -403,7 +387,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService
}.start();
}
- private DictionaryPool getDictionaryPool(final String locale) {
+ public DictionaryPool getDictionaryPool(final String locale) {
DictionaryPool pool = mDictionaryPools.get(locale);
if (null == pool) {
final Locale localeObject = LocaleUtils.constructLocaleFromString(locale);
@@ -424,36 +408,20 @@ public class AndroidSpellCheckerService extends SpellCheckerService
DictionaryFactory.createMainDictionaryFromManager(this, locale,
true /* useFullEditDistance */);
final String localeStr = locale.toString();
- Dictionary userDictionary = mUserDictionaries.get(localeStr);
+ UserBinaryDictionary userDictionary = mUserDictionaries.get(localeStr);
if (null == userDictionary) {
- if (LatinIME.USE_BINARY_USER_DICTIONARY) {
- userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true);
- } else {
- userDictionary = new SynchronouslyLoadedUserDictionary(this, localeStr, true);
- }
+ userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true);
mUserDictionaries.put(localeStr, userDictionary);
}
dictionaryCollection.addDictionary(userDictionary);
- Dictionary whitelistDictionary = mWhitelistDictionaries.get(localeStr);
- if (null == whitelistDictionary) {
- whitelistDictionary = new WhitelistDictionary(this, locale);
- mWhitelistDictionaries.put(localeStr, whitelistDictionary);
- }
- dictionaryCollection.addDictionary(whitelistDictionary);
synchronized (mUseContactsLock) {
if (mUseContactsDictionary) {
if (null == mContactsDictionary) {
- // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no
- // longer needed
- if (LatinIME.USE_BINARY_CONTACTS_DICTIONARY) {
- // TODO: use the right locale. We can't do it right now because the
- // spell checker is reusing the contacts dictionary across sessions
- // without regard for their locale, so we need to fix that first.
- mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this,
- Locale.getDefault());
- } else {
- mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
- }
+ // TODO: use the right locale. We can't do it right now because the
+ // spell checker is reusing the contacts dictionary across sessions
+ // without regard for their locale, so we need to fix that first.
+ mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this,
+ Locale.getDefault());
}
}
dictionaryCollection.addDictionary(mContactsDictionary);
@@ -464,7 +432,7 @@ public class AndroidSpellCheckerService extends SpellCheckerService
}
// This method assumes the text is not empty or null.
- private static int getCapitalizationType(String text) {
+ public static int getCapitalizationType(String text) {
// If the first char is not uppercase, then the word is either all lower case,
// and in either case we return CAPITALIZE_NONE.
if (!Character.isUpperCase(text.codePointAt(0))) return CAPITALIZE_NONE;
@@ -481,378 +449,4 @@ public class AndroidSpellCheckerService extends SpellCheckerService
if (1 == capsCount) return CAPITALIZE_FIRST;
return (len == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE);
}
-
- private static class AndroidSpellCheckerSession extends Session {
- // Immutable, but need the locale which is not available in the constructor yet
- private DictionaryPool mDictionaryPool;
- // Likewise
- private Locale mLocale;
- // Cache this for performance
- private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now.
-
- private final AndroidSpellCheckerService mService;
-
- private final SuggestionsCache mSuggestionsCache = new SuggestionsCache();
- private final ContentObserver mObserver;
-
- private static class SuggestionsParams {
- public final String[] mSuggestions;
- public final int mFlags;
- public SuggestionsParams(String[] suggestions, int flags) {
- mSuggestions = suggestions;
- mFlags = flags;
- }
- }
-
- private static class SuggestionsCache {
- private static final int MAX_CACHE_SIZE = 50;
- // TODO: support bigram
- private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
- new LruCache<String, SuggestionsParams>(MAX_CACHE_SIZE);
-
- public SuggestionsParams getSuggestionsFromCache(String query) {
- return mUnigramSuggestionsInfoCache.get(query);
- }
-
- public void putSuggestionsToCache(String query, String[] suggestions, int flags) {
- if (suggestions == null || TextUtils.isEmpty(query)) {
- return;
- }
- mUnigramSuggestionsInfoCache.put(query, new SuggestionsParams(suggestions, flags));
- }
-
- public void clearCache() {
- mUnigramSuggestionsInfoCache.evictAll();
- }
- }
-
- AndroidSpellCheckerSession(final AndroidSpellCheckerService service) {
- mService = service;
- final ContentResolver cres = service.getContentResolver();
-
- mObserver = new ContentObserver(null) {
- @Override
- public void onChange(boolean self) {
- mSuggestionsCache.clearCache();
- }
- };
- cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
- }
-
- @Override
- public void onCreate() {
- final String localeString = getLocale();
- mDictionaryPool = mService.getDictionaryPool(localeString);
- mLocale = LocaleUtils.constructLocaleFromString(localeString);
- mScript = getScriptFromLocale(mLocale);
- }
-
- @Override
- public void onClose() {
- final ContentResolver cres = mService.getContentResolver();
- cres.unregisterContentObserver(mObserver);
- }
-
- /*
- * Returns whether the code point is a letter that makes sense for the specified
- * locale for this spell checker.
- * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
- * and is limited to EFIGS languages and Russian.
- * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
- * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
- */
- private static boolean isLetterCheckableByLanguage(final int codePoint,
- final int script) {
- switch (script) {
- case SCRIPT_LATIN:
- // Our supported latin script dictionaries (EFIGS) at the moment only include
- // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
- // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
- // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
- // excluded from isLetter anyway.
- return codePoint <= 0x2AF && Character.isLetter(codePoint);
- case SCRIPT_CYRILLIC:
- // All Cyrillic characters are in the 400~52F block. There are some in the upper
- // Unicode range, but they are archaic characters that are not used in modern
- // russian and are not used by our dictionary.
- return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
- default:
- // Should never come here
- throw new RuntimeException("Impossible value of script: " + script);
- }
- }
-
- /**
- * Finds out whether a particular string should be filtered out of spell checking.
- *
- * This will loosely match URLs, numbers, symbols. To avoid always underlining words that
- * we know we will never recognize, this accepts a script identifier that should be one
- * of the SCRIPT_* constants defined above, to rule out quickly characters from very
- * different languages.
- *
- * @param text the string to evaluate.
- * @param script the identifier for the script this spell checker recognizes
- * @return true if we should filter this text out, false otherwise
- */
- private static boolean shouldFilterOut(final String text, final int script) {
- if (TextUtils.isEmpty(text) || text.length() <= 1) return true;
-
- // TODO: check if an equivalent processing can't be done more quickly with a
- // compiled regexp.
- // Filter by first letter
- final int firstCodePoint = text.codePointAt(0);
- // Filter out words that don't start with a letter or an apostrophe
- if (!isLetterCheckableByLanguage(firstCodePoint, script)
- && '\'' != firstCodePoint) return true;
-
- // Filter contents
- final int length = text.length();
- int letterCount = 0;
- for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
- final int codePoint = text.codePointAt(i);
- // Any word containing a '@' is probably an e-mail address
- // Any word containing a '/' is probably either an ad-hoc combination of two
- // words or a URI - in either case we don't want to spell check that
- if ('@' == codePoint || '/' == codePoint) return true;
- if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
- }
- // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
- // in this word are letters
- return (letterCount * 4 < length * 3);
- }
-
- private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(
- TextInfo ti, SentenceSuggestionsInfo ssi) {
- final String typedText = ti.getText();
- if (!typedText.contains(SINGLE_QUOTE)) {
- return null;
- }
- final int N = ssi.getSuggestionsCount();
- final ArrayList<Integer> additionalOffsets = new ArrayList<Integer>();
- final ArrayList<Integer> additionalLengths = new ArrayList<Integer>();
- final ArrayList<SuggestionsInfo> additionalSuggestionsInfos =
- new ArrayList<SuggestionsInfo>();
- for (int i = 0; i < N; ++i) {
- final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
- final int flags = si.getSuggestionsAttributes();
- if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) {
- continue;
- }
- final int offset = ssi.getOffsetAt(i);
- final int length = ssi.getLengthAt(i);
- final String subText = typedText.substring(offset, offset + length);
- if (!subText.contains(SINGLE_QUOTE)) {
- continue;
- }
- final String[] splitTexts = subText.split(SINGLE_QUOTE, -1);
- if (splitTexts == null || splitTexts.length <= 1) {
- continue;
- }
- final int splitNum = splitTexts.length;
- for (int j = 0; j < splitNum; ++j) {
- final String splitText = splitTexts[j];
- if (TextUtils.isEmpty(splitText)) {
- continue;
- }
- if (mSuggestionsCache.getSuggestionsFromCache(splitText) == null) {
- continue;
- }
- final int newLength = splitText.length();
- // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO
- final int newFlags = 0;
- final SuggestionsInfo newSi = new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY);
- newSi.setCookieAndSequence(si.getCookie(), si.getSequence());
- if (DBG) {
- Log.d(TAG, "Override and remove old span over: "
- + splitText + ", " + offset + "," + newLength);
- }
- additionalOffsets.add(offset);
- additionalLengths.add(newLength);
- additionalSuggestionsInfos.add(newSi);
- }
- }
- final int additionalSize = additionalOffsets.size();
- if (additionalSize <= 0) {
- return null;
- }
- final int suggestionsSize = N + additionalSize;
- final int[] newOffsets = new int[suggestionsSize];
- final int[] newLengths = new int[suggestionsSize];
- final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize];
- int i;
- for (i = 0; i < N; ++i) {
- newOffsets[i] = ssi.getOffsetAt(i);
- newLengths[i] = ssi.getLengthAt(i);
- newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i);
- }
- for (; i < suggestionsSize; ++i) {
- newOffsets[i] = additionalOffsets.get(i - N);
- newLengths[i] = additionalLengths.get(i - N);
- newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N);
- }
- return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths);
- }
-
- @Override
- public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(
- TextInfo[] textInfos, int suggestionsLimit) {
- final SentenceSuggestionsInfo[] retval = super.onGetSentenceSuggestionsMultiple(
- textInfos, suggestionsLimit);
- if (retval == null || retval.length != textInfos.length) {
- return retval;
- }
- for (int i = 0; i < retval.length; ++i) {
- final SentenceSuggestionsInfo tempSsi =
- fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]);
- if (tempSsi != null) {
- retval[i] = tempSsi;
- }
- }
- return retval;
- }
-
- @Override
- public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
- int suggestionsLimit, boolean sequentialWords) {
- final int length = textInfos.length;
- final SuggestionsInfo[] retval = new SuggestionsInfo[length];
- for (int i = 0; i < length; ++i) {
- final String prevWord;
- if (sequentialWords && i > 0) {
- final String prevWordCandidate = textInfos[i - 1].getText();
- // Note that an empty string would be used to indicate the initial word
- // in the future.
- prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate;
- } else {
- prevWord = null;
- }
- retval[i] = onGetSuggestions(textInfos[i], prevWord, suggestionsLimit);
- retval[i].setCookieAndSequence(
- textInfos[i].getCookie(), textInfos[i].getSequence());
- }
- return retval;
- }
-
- // Note : this must be reentrant
- /**
- * Gets a list of suggestions for a specific string. This returns a list of possible
- * corrections for the text passed as an argument. It may split or group words, and
- * even perform grammatical analysis.
- */
- @Override
- public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
- final int suggestionsLimit) {
- return onGetSuggestions(textInfo, null, suggestionsLimit);
- }
-
- private SuggestionsInfo onGetSuggestions(
- final TextInfo textInfo, final String prevWord, final int suggestionsLimit) {
- try {
- final String inText = textInfo.getText();
- final SuggestionsParams cachedSuggestionsParams =
- mSuggestionsCache.getSuggestionsFromCache(inText);
- if (cachedSuggestionsParams != null) {
- if (DBG) {
- Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags);
- }
- return new SuggestionsInfo(
- cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
- }
-
- if (shouldFilterOut(inText, mScript)) {
- DictAndProximity dictInfo = null;
- try {
- dictInfo = mDictionaryPool.takeOrGetNull();
- if (null == dictInfo) return getNotInDictEmptySuggestions();
- return dictInfo.mDictionary.isValidWord(inText) ?
- getInDictEmptySuggestions() : getNotInDictEmptySuggestions();
- } finally {
- if (null != dictInfo) {
- if (!mDictionaryPool.offer(dictInfo)) {
- Log.e(TAG, "Can't re-insert a dictionary into its pool");
- }
- }
- }
- }
- final String text = inText.replaceAll(APOSTROPHE, SINGLE_QUOTE);
-
- // TODO: Don't gather suggestions if the limit is <= 0 unless necessary
- final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text,
- mService.mSuggestionThreshold, mService.mRecommendedThreshold,
- suggestionsLimit);
- final WordComposer composer = new WordComposer();
- final int length = text.length();
- for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
- final int codePoint = text.codePointAt(i);
- // The getXYForCodePointAndScript method returns (Y << 16) + X
- final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
- codePoint, mScript);
- if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
- composer.add(codePoint, WordComposer.NOT_A_COORDINATE,
- WordComposer.NOT_A_COORDINATE, null);
- } else {
- composer.add(codePoint, xy & 0xFFFF, xy >> 16, null);
- }
- }
-
- final int capitalizeType = getCapitalizationType(text);
- boolean isInDict = true;
- DictAndProximity dictInfo = null;
- try {
- dictInfo = mDictionaryPool.takeOrGetNull();
- if (null == dictInfo) return getNotInDictEmptySuggestions();
- dictInfo.mDictionary.getWords(composer, prevWord, suggestionsGatherer,
- dictInfo.mProximityInfo);
- isInDict = dictInfo.mDictionary.isValidWord(text);
- if (!isInDict && CAPITALIZE_NONE != capitalizeType) {
- // We want to test the word again if it's all caps or first caps only.
- // If it's fully down, we already tested it, if it's mixed case, we don't
- // want to test a lowercase version of it.
- isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
- }
- } finally {
- if (null != dictInfo) {
- if (!mDictionaryPool.offer(dictInfo)) {
- Log.e(TAG, "Can't re-insert a dictionary into its pool");
- }
- }
- }
-
- final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(
- capitalizeType, mLocale);
-
- if (DBG) {
- Log.i(TAG, "Spell checking results for " + text + " with suggestion limit "
- + suggestionsLimit);
- Log.i(TAG, "IsInDict = " + isInDict);
- Log.i(TAG, "LooksLikeTypo = " + (!isInDict));
- Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions);
- if (null != result.mSuggestions) {
- for (String suggestion : result.mSuggestions) {
- Log.i(TAG, suggestion);
- }
- }
- }
-
- final int flags =
- (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY
- : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO)
- | (result.mHasRecommendedSuggestions
- ? SuggestionsInfoCompatUtils
- .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
- : 0);
- final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
- mSuggestionsCache.putSuggestionsToCache(text, result.mSuggestions, flags);
- return retval;
- } catch (RuntimeException e) {
- // Don't kill the keyboard if there is a bug in the spell checker
- if (DBG) {
- throw e;
- } else {
- Log.e(TAG, "Exception while spellcheking: " + e);
- return getNotInDictEmptySuggestions();
- }
- }
- }
- }
}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
new file mode 100644
index 000000000..668e7a641
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSession.java
@@ -0,0 +1,154 @@
+/*
+ * 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.spellcheck;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.textservice.SentenceSuggestionsInfo;
+import android.view.textservice.SuggestionsInfo;
+import android.view.textservice.TextInfo;
+
+import com.android.inputmethod.latin.CollectionUtils;
+
+import java.util.ArrayList;
+
+public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheckerSession {
+ private static final String TAG = AndroidSpellCheckerSession.class.getSimpleName();
+ private static final boolean DBG = false;
+ private final static String[] EMPTY_STRING_ARRAY = new String[0];
+
+ public AndroidSpellCheckerSession(AndroidSpellCheckerService service) {
+ super(service);
+ }
+
+ private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(TextInfo ti,
+ SentenceSuggestionsInfo ssi) {
+ final String typedText = ti.getText();
+ if (!typedText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
+ return null;
+ }
+ final int N = ssi.getSuggestionsCount();
+ final ArrayList<Integer> additionalOffsets = CollectionUtils.newArrayList();
+ final ArrayList<Integer> additionalLengths = CollectionUtils.newArrayList();
+ final ArrayList<SuggestionsInfo> additionalSuggestionsInfos =
+ CollectionUtils.newArrayList();
+ String currentWord = null;
+ for (int i = 0; i < N; ++i) {
+ final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
+ final int flags = si.getSuggestionsAttributes();
+ if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) {
+ continue;
+ }
+ final int offset = ssi.getOffsetAt(i);
+ final int length = ssi.getLengthAt(i);
+ final String subText = typedText.substring(offset, offset + length);
+ final String prevWord = currentWord;
+ currentWord = subText;
+ if (!subText.contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
+ continue;
+ }
+ final String[] splitTexts =
+ subText.split(AndroidSpellCheckerService.SINGLE_QUOTE, -1);
+ if (splitTexts == null || splitTexts.length <= 1) {
+ continue;
+ }
+ final int splitNum = splitTexts.length;
+ for (int j = 0; j < splitNum; ++j) {
+ final String splitText = splitTexts[j];
+ if (TextUtils.isEmpty(splitText)) {
+ continue;
+ }
+ if (mSuggestionsCache.getSuggestionsFromCache(splitText, prevWord) == null) {
+ continue;
+ }
+ final int newLength = splitText.length();
+ // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO
+ final int newFlags = 0;
+ final SuggestionsInfo newSi =
+ new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY);
+ newSi.setCookieAndSequence(si.getCookie(), si.getSequence());
+ if (DBG) {
+ Log.d(TAG, "Override and remove old span over: " + splitText + ", "
+ + offset + "," + newLength);
+ }
+ additionalOffsets.add(offset);
+ additionalLengths.add(newLength);
+ additionalSuggestionsInfos.add(newSi);
+ }
+ }
+ final int additionalSize = additionalOffsets.size();
+ if (additionalSize <= 0) {
+ return null;
+ }
+ final int suggestionsSize = N + additionalSize;
+ final int[] newOffsets = new int[suggestionsSize];
+ final int[] newLengths = new int[suggestionsSize];
+ final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize];
+ int i;
+ for (i = 0; i < N; ++i) {
+ newOffsets[i] = ssi.getOffsetAt(i);
+ newLengths[i] = ssi.getLengthAt(i);
+ newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i);
+ }
+ for (; i < suggestionsSize; ++i) {
+ newOffsets[i] = additionalOffsets.get(i - N);
+ newLengths[i] = additionalLengths.get(i - N);
+ newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N);
+ }
+ return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths);
+ }
+
+ @Override
+ public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos,
+ int suggestionsLimit) {
+ final SentenceSuggestionsInfo[] retval =
+ super.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit);
+ if (retval == null || retval.length != textInfos.length) {
+ return retval;
+ }
+ for (int i = 0; i < retval.length; ++i) {
+ final SentenceSuggestionsInfo tempSsi =
+ fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]);
+ if (tempSsi != null) {
+ retval[i] = tempSsi;
+ }
+ }
+ return retval;
+ }
+
+ @Override
+ public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
+ int suggestionsLimit, boolean sequentialWords) {
+ final int length = textInfos.length;
+ final SuggestionsInfo[] retval = new SuggestionsInfo[length];
+ for (int i = 0; i < length; ++i) {
+ final String prevWord;
+ if (sequentialWords && i > 0) {
+ final String prevWordCandidate = textInfos[i - 1].getText();
+ // Note that an empty string would be used to indicate the initial word
+ // in the future.
+ prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate;
+ } else {
+ prevWord = null;
+ }
+ retval[i] = onGetSuggestions(textInfos[i], prevWord, suggestionsLimit);
+ retval[i].setCookieAndSequence(textInfos[i].getCookie(),
+ textInfos[i].getSequence());
+ }
+ return retval;
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java
new file mode 100644
index 000000000..8eb1eb68e
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerSessionFactory.java
@@ -0,0 +1,25 @@
+/*
+ * 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.spellcheck;
+
+import android.service.textservice.SpellCheckerService.Session;
+
+public abstract class AndroidSpellCheckerSessionFactory {
+ public static Session newInstance(AndroidSpellCheckerService service) {
+ return new AndroidSpellCheckerSession(service);
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
new file mode 100644
index 000000000..53ed4d3c3
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -0,0 +1,326 @@
+/*
+ * 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.spellcheck;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.provider.UserDictionary.Words;
+import android.service.textservice.SpellCheckerService.Session;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LruCache;
+import android.view.textservice.SuggestionsInfo;
+import android.view.textservice.TextInfo;
+
+import com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.LocaleUtils;
+import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.spellcheck.AndroidSpellCheckerService.SuggestionsGatherer;
+
+import java.util.ArrayList;
+import java.util.Locale;
+
+public abstract class AndroidWordLevelSpellCheckerSession extends Session {
+ private static final String TAG = AndroidWordLevelSpellCheckerSession.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ // Immutable, but need the locale which is not available in the constructor yet
+ private DictionaryPool mDictionaryPool;
+ // Likewise
+ private Locale mLocale;
+ // Cache this for performance
+ private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now.
+ private final AndroidSpellCheckerService mService;
+ protected final SuggestionsCache mSuggestionsCache = new SuggestionsCache();
+ private final ContentObserver mObserver;
+
+ private static final class SuggestionsParams {
+ public final String[] mSuggestions;
+ public final int mFlags;
+ public SuggestionsParams(String[] suggestions, int flags) {
+ mSuggestions = suggestions;
+ mFlags = flags;
+ }
+ }
+
+ protected static final class SuggestionsCache {
+ private static final char CHAR_DELIMITER = '\uFFFC';
+ private static final int MAX_CACHE_SIZE = 50;
+ private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
+ new LruCache<String, SuggestionsParams>(MAX_CACHE_SIZE);
+
+ // TODO: Support n-gram input
+ private static String generateKey(String query, String prevWord) {
+ if (TextUtils.isEmpty(query) || TextUtils.isEmpty(prevWord)) {
+ return query;
+ }
+ return query + CHAR_DELIMITER + prevWord;
+ }
+
+ // TODO: Support n-gram input
+ public SuggestionsParams getSuggestionsFromCache(String query, String prevWord) {
+ return mUnigramSuggestionsInfoCache.get(generateKey(query, prevWord));
+ }
+
+ // TODO: Support n-gram input
+ public void putSuggestionsToCache(
+ String query, String prevWord, String[] suggestions, int flags) {
+ if (suggestions == null || TextUtils.isEmpty(query)) {
+ return;
+ }
+ mUnigramSuggestionsInfoCache.put(
+ generateKey(query, prevWord), new SuggestionsParams(suggestions, flags));
+ }
+
+ public void clearCache() {
+ mUnigramSuggestionsInfoCache.evictAll();
+ }
+ }
+
+ AndroidWordLevelSpellCheckerSession(final AndroidSpellCheckerService service) {
+ mService = service;
+ final ContentResolver cres = service.getContentResolver();
+
+ mObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean self) {
+ mSuggestionsCache.clearCache();
+ }
+ };
+ cres.registerContentObserver(Words.CONTENT_URI, true, mObserver);
+ }
+
+ @Override
+ public void onCreate() {
+ final String localeString = getLocale();
+ mDictionaryPool = mService.getDictionaryPool(localeString);
+ mLocale = LocaleUtils.constructLocaleFromString(localeString);
+ mScript = AndroidSpellCheckerService.getScriptFromLocale(mLocale);
+ }
+
+ @Override
+ public void onClose() {
+ final ContentResolver cres = mService.getContentResolver();
+ cres.unregisterContentObserver(mObserver);
+ }
+
+ /*
+ * Returns whether the code point is a letter that makes sense for the specified
+ * locale for this spell checker.
+ * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
+ * and is limited to EFIGS languages and Russian.
+ * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
+ * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
+ */
+ private static boolean isLetterCheckableByLanguage(final int codePoint,
+ final int script) {
+ switch (script) {
+ case AndroidSpellCheckerService.SCRIPT_LATIN:
+ // Our supported latin script dictionaries (EFIGS) at the moment only include
+ // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
+ // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
+ // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
+ // excluded from isLetter anyway.
+ return codePoint <= 0x2AF && Character.isLetter(codePoint);
+ case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
+ // All Cyrillic characters are in the 400~52F block. There are some in the upper
+ // Unicode range, but they are archaic characters that are not used in modern
+ // russian and are not used by our dictionary.
+ return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
+ default:
+ // Should never come here
+ throw new RuntimeException("Impossible value of script: " + script);
+ }
+ }
+
+ /**
+ * Finds out whether a particular string should be filtered out of spell checking.
+ *
+ * This will loosely match URLs, numbers, symbols. To avoid always underlining words that
+ * we know we will never recognize, this accepts a script identifier that should be one
+ * of the SCRIPT_* constants defined above, to rule out quickly characters from very
+ * different languages.
+ *
+ * @param text the string to evaluate.
+ * @param script the identifier for the script this spell checker recognizes
+ * @return true if we should filter this text out, false otherwise
+ */
+ private static boolean shouldFilterOut(final String text, final int script) {
+ if (TextUtils.isEmpty(text) || text.length() <= 1) return true;
+
+ // TODO: check if an equivalent processing can't be done more quickly with a
+ // compiled regexp.
+ // Filter by first letter
+ final int firstCodePoint = text.codePointAt(0);
+ // Filter out words that don't start with a letter or an apostrophe
+ if (!isLetterCheckableByLanguage(firstCodePoint, script)
+ && '\'' != firstCodePoint) return true;
+
+ // Filter contents
+ final int length = text.length();
+ int letterCount = 0;
+ for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
+ final int codePoint = text.codePointAt(i);
+ // Any word containing a '@' is probably an e-mail address
+ // Any word containing a '/' is probably either an ad-hoc combination of two
+ // words or a URI - in either case we don't want to spell check that
+ if ('@' == codePoint || '/' == codePoint) return true;
+ if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
+ }
+ // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
+ // in this word are letters
+ return (letterCount * 4 < length * 3);
+ }
+
+ // Note : this must be reentrant
+ /**
+ * Gets a list of suggestions for a specific string. This returns a list of possible
+ * corrections for the text passed as an argument. It may split or group words, and
+ * even perform grammatical analysis.
+ */
+ @Override
+ public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
+ final int suggestionsLimit) {
+ return onGetSuggestions(textInfo, null, suggestionsLimit);
+ }
+
+ protected SuggestionsInfo onGetSuggestions(
+ final TextInfo textInfo, final String prevWord, final int suggestionsLimit) {
+ try {
+ final String inText = textInfo.getText();
+ final SuggestionsParams cachedSuggestionsParams =
+ mSuggestionsCache.getSuggestionsFromCache(inText, prevWord);
+ if (cachedSuggestionsParams != null) {
+ if (DBG) {
+ Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags);
+ }
+ return new SuggestionsInfo(
+ cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
+ }
+
+ if (shouldFilterOut(inText, mScript)) {
+ DictAndProximity dictInfo = null;
+ try {
+ dictInfo = mDictionaryPool.pollWithDefaultTimeout();
+ if (!DictionaryPool.isAValidDictionary(dictInfo)) {
+ return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+ }
+ return dictInfo.mDictionary.isValidWord(inText)
+ ? AndroidSpellCheckerService.getInDictEmptySuggestions()
+ : AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+ } finally {
+ if (null != dictInfo) {
+ if (!mDictionaryPool.offer(dictInfo)) {
+ Log.e(TAG, "Can't re-insert a dictionary into its pool");
+ }
+ }
+ }
+ }
+ final String text = inText.replaceAll(
+ AndroidSpellCheckerService.APOSTROPHE, AndroidSpellCheckerService.SINGLE_QUOTE);
+
+ // TODO: Don't gather suggestions if the limit is <= 0 unless necessary
+ //final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text,
+ //mService.mSuggestionThreshold, mService.mRecommendedThreshold,
+ //suggestionsLimit);
+ final SuggestionsGatherer suggestionsGatherer = mService.newSuggestionsGatherer(
+ text, suggestionsLimit);
+ final WordComposer composer = new WordComposer();
+ final int length = text.length();
+ for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
+ final int codePoint = text.codePointAt(i);
+ // The getXYForCodePointAndScript method returns (Y << 16) + X
+ final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
+ codePoint, mScript);
+ if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
+ composer.add(codePoint,
+ Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+ } else {
+ composer.add(codePoint, xy & 0xFFFF, xy >> 16);
+ }
+ }
+
+ final int capitalizeType = AndroidSpellCheckerService.getCapitalizationType(text);
+ boolean isInDict = true;
+ DictAndProximity dictInfo = null;
+ try {
+ dictInfo = mDictionaryPool.pollWithDefaultTimeout();
+ if (!DictionaryPool.isAValidDictionary(dictInfo)) {
+ return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+ }
+ final ArrayList<SuggestedWordInfo> suggestions =
+ dictInfo.mDictionary.getSuggestions(composer, prevWord,
+ dictInfo.mProximityInfo);
+ for (final SuggestedWordInfo suggestion : suggestions) {
+ final String suggestionStr = suggestion.mWord.toString();
+ suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
+ suggestionStr.length(), suggestion.mScore);
+ }
+ isInDict = dictInfo.mDictionary.isValidWord(text);
+ if (!isInDict && AndroidSpellCheckerService.CAPITALIZE_NONE != capitalizeType) {
+ // We want to test the word again if it's all caps or first caps only.
+ // If it's fully down, we already tested it, if it's mixed case, we don't
+ // want to test a lowercase version of it.
+ isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
+ }
+ } finally {
+ if (null != dictInfo) {
+ if (!mDictionaryPool.offer(dictInfo)) {
+ Log.e(TAG, "Can't re-insert a dictionary into its pool");
+ }
+ }
+ }
+
+ final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(
+ capitalizeType, mLocale);
+
+ if (DBG) {
+ Log.i(TAG, "Spell checking results for " + text + " with suggestion limit "
+ + suggestionsLimit);
+ Log.i(TAG, "IsInDict = " + isInDict);
+ Log.i(TAG, "LooksLikeTypo = " + (!isInDict));
+ Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions);
+ if (null != result.mSuggestions) {
+ for (String suggestion : result.mSuggestions) {
+ Log.i(TAG, suggestion);
+ }
+ }
+ }
+
+ final int flags =
+ (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY
+ : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO)
+ | (result.mHasRecommendedSuggestions
+ ? SuggestionsInfoCompatUtils
+ .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
+ : 0);
+ final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
+ mSuggestionsCache.putSuggestionsToCache(text, prevWord, result.mSuggestions, flags);
+ return retval;
+ } catch (RuntimeException e) {
+ // Don't kill the keyboard if there is a bug in the spell checker
+ if (DBG) {
+ throw e;
+ } else {
+ Log.e(TAG, "Exception while spellcheking: " + e);
+ return AndroidSpellCheckerService.getNotInDictEmptySuggestions();
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java b/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java
index 3dbbd40cd..9d7c61a33 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java
@@ -22,7 +22,7 @@ import com.android.inputmethod.keyboard.ProximityInfo;
/**
* A simple container for both a Dictionary and a ProximityInfo.
*/
-public class DictAndProximity {
+public final class DictAndProximity {
public final Dictionary mDictionary;
public final ProximityInfo mProximityInfo;
public DictAndProximity(final Dictionary dictionary, final ProximityInfo proximityInfo) {
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
index 8fc632ee7..1fb2bbb6a 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
@@ -16,19 +16,56 @@
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 java.util.ArrayList;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
/**
* A blocking queue that creates dictionaries up to a certain limit as necessary.
+ * As a deadlock-detecting device, if waiting for more than TIMEOUT = 3 seconds, we
+ * will clear the queue and generate its contents again. This is transparent for
+ * the client code, but may help with sloppy clients.
*/
@SuppressWarnings("serial")
-public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
+public final class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
+ private final static String TAG = DictionaryPool.class.getSimpleName();
+ // How many seconds we wait for a dictionary to become available. Past this delay, we give up in
+ // fear some bug caused a deadlock, and reset the whole pool.
+ private final static int TIMEOUT = 3;
private final AndroidSpellCheckerService mService;
private final int mMaxSize;
private final Locale mLocale;
private int mSize;
private volatile boolean mClosed;
+ final static ArrayList<SuggestedWordInfo> noSuggestions = CollectionUtils.newArrayList();
+ private final static DictAndProximity dummyDict = new DictAndProximity(
+ new Dictionary(Dictionary.TYPE_MAIN) {
+ @Override
+ public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
+ final CharSequence prevWord, final ProximityInfo proximityInfo) {
+ return noSuggestions;
+ }
+ @Override
+ public boolean isValidWord(CharSequence word) {
+ // This is never called. However if for some strange reason it ever gets
+ // called, returning true is less destructive (it will not underline the
+ // word in red).
+ return true;
+ }
+ }, null);
+
+ static public boolean isAValidDictionary(final DictAndProximity dictInfo) {
+ return null != dictInfo && dummyDict != dictInfo;
+ }
public DictionaryPool(final int maxSize, final AndroidSpellCheckerService service,
final Locale locale) {
@@ -41,13 +78,23 @@ public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
}
@Override
- public DictAndProximity take() throws InterruptedException {
+ public DictAndProximity poll(final long timeout, final TimeUnit unit)
+ throws InterruptedException {
final DictAndProximity dict = poll();
if (null != dict) return dict;
synchronized(this) {
if (mSize >= mMaxSize) {
- // Our pool is already full. Wait until some dictionary is ready.
- return super.take();
+ // Our pool is already full. Wait until some dictionary is ready, or TIMEOUT
+ // expires to avoid a deadlock.
+ final DictAndProximity result = super.poll(timeout, unit);
+ if (null == result) {
+ Log.e(TAG, "Deadlock detected ! Resetting dictionary pool");
+ clear();
+ mSize = 1;
+ return mService.createDictAndProximity(mLocale);
+ } else {
+ return result;
+ }
} else {
++mSize;
return mService.createDictAndProximity(mLocale);
@@ -56,9 +103,9 @@ public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
}
// Convenience method
- public DictAndProximity takeOrGetNull() {
+ public DictAndProximity pollWithDefaultTimeout() {
try {
- return take();
+ return poll(TIMEOUT, TimeUnit.SECONDS);
} catch (InterruptedException e) {
return null;
}
@@ -78,7 +125,7 @@ public class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
public boolean offer(final DictAndProximity dict) {
if (mClosed) {
dict.mDictionary.close();
- return false;
+ return super.offer(dummyDict);
} else {
return super.offer(dict);
}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
index 0103e8423..11bb97031 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
@@ -16,14 +16,15 @@
package com.android.inputmethod.latin.spellcheck;
-import com.android.inputmethod.keyboard.KeyDetector;
import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.Constants;
import java.util.TreeMap;
-public class SpellCheckerProximityInfo {
+public final class SpellCheckerProximityInfo {
/* public for test */
- final public static int NUL = KeyDetector.NOT_A_CODE;
+ final public static int NUL = Constants.NOT_A_CODE;
// This must be the same as MAX_PROXIMITY_CHARS_SIZE else it will not work inside
// native code - this value is passed at creation of the binary object and reused
@@ -52,14 +53,14 @@ public class SpellCheckerProximityInfo {
return result;
}
- private static class Latin {
+ private static final class Latin {
// This is a map from the code point to the index in the PROXIMITY array.
// At the time the native code to read the binary dictionary needs the proximity info be
// passed as a flat array spaced by MAX_PROXIMITY_CHARS_SIZE columns, one for each input
// character.
// Since we need to build such an array, we want to be able to search in our big proximity
// data quickly by character, and a map is probably the best way to do this.
- final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>();
+ final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap();
// The proximity here is the union of
// - the proximity for a QWERTY keyboard.
@@ -111,6 +112,7 @@ public class SpellCheckerProximityInfo {
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
+ NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
};
static {
buildProximityIndices(PROXIMITY, INDICES);
@@ -120,8 +122,8 @@ public class SpellCheckerProximityInfo {
}
}
- private static class Cyrillic {
- final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>();
+ private static final class Cyrillic {
+ final private static TreeMap<Integer, Integer> INDICES = CollectionUtils.newTreeMap();
// TODO: The following table is solely based on the keyboard layout. Consult with Russian
// speakers on commonly misspelled words/letters.
final static int[] PROXIMITY = {
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
index e14db8797..e63dff312 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsActivity.java
@@ -23,7 +23,7 @@ import android.preference.PreferenceActivity;
/**
* Spell checker preference screen.
*/
-public class SpellCheckerSettingsActivity extends PreferenceActivity {
+public final class SpellCheckerSettingsActivity extends PreferenceActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
index 7056874a1..ef5123d68 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerSettingsFragment.java
@@ -24,7 +24,7 @@ import com.android.inputmethod.latin.R;
/**
* Preference screen.
*/
-public class SpellCheckerSettingsFragment extends PreferenceFragment {
+public final class SpellCheckerSettingsFragment extends PreferenceFragment {
/**
* Empty constructor for fragment generation.
*/
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
index c6fe43b69..4e9fd1968 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestions.java
@@ -23,158 +23,166 @@ import android.graphics.drawable.Drawable;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
+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.Utils;
-public class MoreSuggestions extends Keyboard {
+public final class MoreSuggestions extends Keyboard {
public static final int SUGGESTION_CODE_BASE = 1024;
- MoreSuggestions(Builder.MoreSuggestionsParam params) {
+ MoreSuggestions(final MoreSuggestionsParam params) {
super(params);
}
- public static class Builder extends Keyboard.Builder<Builder.MoreSuggestionsParam> {
- private final MoreSuggestionsView mPaneView;
- private SuggestedWords mSuggestions;
- private int mFromPos;
- private int mToPos;
+ private static final class MoreSuggestionsParam extends KeyboardParams {
+ private final int[] mWidths = new int[SuggestionStripView.MAX_SUGGESTIONS];
+ private final int[] mRowNumbers = new int[SuggestionStripView.MAX_SUGGESTIONS];
+ private final int[] mColumnOrders = new int[SuggestionStripView.MAX_SUGGESTIONS];
+ private final int[] mNumColumnsInRow = new int[SuggestionStripView.MAX_SUGGESTIONS];
+ private static final int MAX_COLUMNS_IN_ROW = 3;
+ private int mNumRows;
+ public Drawable mDivider;
+ public int mDividerWidth;
+
+ public MoreSuggestionsParam() {
+ super();
+ }
- public static class MoreSuggestionsParam extends Keyboard.Params {
- private final int[] mWidths = new int[SuggestionsView.MAX_SUGGESTIONS];
- private final int[] mRowNumbers = new int[SuggestionsView.MAX_SUGGESTIONS];
- private final int[] mColumnOrders = new int[SuggestionsView.MAX_SUGGESTIONS];
- private final int[] mNumColumnsInRow = new int[SuggestionsView.MAX_SUGGESTIONS];
- private static final int MAX_COLUMNS_IN_ROW = 3;
- private int mNumRows;
- public Drawable mDivider;
- public int mDividerWidth;
-
- public int layout(SuggestedWords suggestions, int fromPos, int maxWidth, int minWidth,
- int maxRow, MoreSuggestionsView view) {
- clearKeys();
- final Resources res = view.getContext().getResources();
- mDivider = res.getDrawable(R.drawable.more_suggestions_divider);
- mDividerWidth = mDivider.getIntrinsicWidth();
- final int padding = (int) res.getDimension(
- R.dimen.more_suggestions_key_horizontal_padding);
- final Paint paint = view.newDefaultLabelPaint();
-
- int row = 0;
- int pos = fromPos, rowStartPos = fromPos;
- final int size = Math.min(suggestions.size(), SuggestionsView.MAX_SUGGESTIONS);
- while (pos < size) {
- final String word = suggestions.getWord(pos).toString();
- // TODO: Should take care of text x-scaling.
- mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding;
- final int numColumn = pos - rowStartPos + 1;
- final int columnWidth =
- (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn;
- if (numColumn > MAX_COLUMNS_IN_ROW
- || !fitInWidth(rowStartPos, pos + 1, columnWidth)) {
- if ((row + 1) >= maxRow) {
- break;
- }
- mNumColumnsInRow[row] = pos - rowStartPos;
- rowStartPos = pos;
- row++;
+ // TODO: Remove {@link MoreSuggestionsView} argument.
+ public int layout(final SuggestedWords suggestions, final int fromPos, final int maxWidth,
+ final int minWidth, final int maxRow, final MoreSuggestionsView view) {
+ clearKeys();
+ final Resources res = view.getResources();
+ mDivider = res.getDrawable(R.drawable.more_suggestions_divider);
+ mDividerWidth = mDivider.getIntrinsicWidth();
+ final int padding = (int) res.getDimension(
+ R.dimen.more_suggestions_key_horizontal_padding);
+ final Paint paint = view.newDefaultLabelPaint();
+
+ int row = 0;
+ int pos = fromPos, rowStartPos = fromPos;
+ final int size = Math.min(suggestions.size(), SuggestionStripView.MAX_SUGGESTIONS);
+ while (pos < size) {
+ final String word = suggestions.getWord(pos).toString();
+ // TODO: Should take care of text x-scaling.
+ mWidths[pos] = (int)view.getLabelWidth(word, paint) + padding;
+ final int numColumn = pos - rowStartPos + 1;
+ final int columnWidth =
+ (maxWidth - mDividerWidth * (numColumn - 1)) / numColumn;
+ if (numColumn > MAX_COLUMNS_IN_ROW
+ || !fitInWidth(rowStartPos, pos + 1, columnWidth)) {
+ if ((row + 1) >= maxRow) {
+ break;
}
- mColumnOrders[pos] = pos - rowStartPos;
- mRowNumbers[pos] = row;
- pos++;
+ mNumColumnsInRow[row] = pos - rowStartPos;
+ rowStartPos = pos;
+ row++;
}
- mNumColumnsInRow[row] = pos - rowStartPos;
- mNumRows = row + 1;
- mBaseWidth = mOccupiedWidth = Math.max(
- minWidth, calcurateMaxRowWidth(fromPos, pos));
- mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap;
- return pos - fromPos;
+ mColumnOrders[pos] = pos - rowStartPos;
+ mRowNumbers[pos] = row;
+ pos++;
}
+ mNumColumnsInRow[row] = pos - rowStartPos;
+ mNumRows = row + 1;
+ mBaseWidth = mOccupiedWidth = Math.max(
+ minWidth, calcurateMaxRowWidth(fromPos, pos));
+ mBaseHeight = mOccupiedHeight = mNumRows * mDefaultRowHeight + mVerticalGap;
+ return pos - fromPos;
+ }
- private boolean fitInWidth(int startPos, int endPos, int width) {
- for (int pos = startPos; pos < endPos; pos++) {
- if (mWidths[pos] > width)
- return false;
- }
- return true;
+ private boolean fitInWidth(final int startPos, final int endPos, final int width) {
+ for (int pos = startPos; pos < endPos; pos++) {
+ if (mWidths[pos] > width)
+ return false;
}
+ return true;
+ }
- private int calcurateMaxRowWidth(int startPos, int endPos) {
- int maxRowWidth = 0;
- int pos = startPos;
- for (int row = 0; row < mNumRows; row++) {
- final int numColumnInRow = mNumColumnsInRow[row];
- int maxKeyWidth = 0;
- while (pos < endPos && mRowNumbers[pos] == row) {
- maxKeyWidth = Math.max(maxKeyWidth, mWidths[pos]);
- pos++;
- }
- maxRowWidth = Math.max(maxRowWidth,
- maxKeyWidth * numColumnInRow + mDividerWidth * (numColumnInRow - 1));
+ private int calcurateMaxRowWidth(final int startPos, final int endPos) {
+ int maxRowWidth = 0;
+ int pos = startPos;
+ for (int row = 0; row < mNumRows; row++) {
+ final int numColumnInRow = mNumColumnsInRow[row];
+ int maxKeyWidth = 0;
+ while (pos < endPos && mRowNumbers[pos] == row) {
+ maxKeyWidth = Math.max(maxKeyWidth, mWidths[pos]);
+ pos++;
}
- return maxRowWidth;
+ maxRowWidth = Math.max(maxRowWidth,
+ maxKeyWidth * numColumnInRow + mDividerWidth * (numColumnInRow - 1));
}
+ return maxRowWidth;
+ }
- private static final int[][] COLUMN_ORDER_TO_NUMBER = {
- { 0, },
- { 1, 0, },
- { 2, 0, 1},
- };
-
- public int getNumColumnInRow(int pos) {
- return mNumColumnsInRow[mRowNumbers[pos]];
- }
+ private static final int[][] COLUMN_ORDER_TO_NUMBER = {
+ { 0, },
+ { 1, 0, },
+ { 2, 0, 1},
+ };
- public int getColumnNumber(int pos) {
- final int columnOrder = mColumnOrders[pos];
- final int numColumn = getNumColumnInRow(pos);
- return COLUMN_ORDER_TO_NUMBER[numColumn - 1][columnOrder];
- }
+ public int getNumColumnInRow(final int pos) {
+ return mNumColumnsInRow[mRowNumbers[pos]];
+ }
- public int getX(int pos) {
- final int columnNumber = getColumnNumber(pos);
- return columnNumber * (getWidth(pos) + mDividerWidth);
- }
+ public int getColumnNumber(final int pos) {
+ final int columnOrder = mColumnOrders[pos];
+ final int numColumn = getNumColumnInRow(pos);
+ return COLUMN_ORDER_TO_NUMBER[numColumn - 1][columnOrder];
+ }
- public int getY(int pos) {
- final int row = mRowNumbers[pos];
- return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding;
- }
+ public int getX(final int pos) {
+ final int columnNumber = getColumnNumber(pos);
+ return columnNumber * (getWidth(pos) + mDividerWidth);
+ }
- public int getWidth(int pos) {
- final int numColumnInRow = getNumColumnInRow(pos);
- return (mOccupiedWidth - mDividerWidth * (numColumnInRow - 1)) / numColumnInRow;
- }
+ public int getY(final int pos) {
+ final int row = mRowNumbers[pos];
+ return (mNumRows -1 - row) * mDefaultRowHeight + mTopPadding;
+ }
- public void markAsEdgeKey(Key key, int pos) {
- final int row = mRowNumbers[pos];
- if (row == 0)
- key.markAsBottomEdge(this);
- if (row == mNumRows - 1)
- key.markAsTopEdge(this);
+ public int getWidth(final int pos) {
+ final int numColumnInRow = getNumColumnInRow(pos);
+ return (mOccupiedWidth - mDividerWidth * (numColumnInRow - 1)) / numColumnInRow;
+ }
- final int numColumnInRow = mNumColumnsInRow[row];
- final int column = getColumnNumber(pos);
- if (column == 0)
- key.markAsLeftEdge(this);
- if (column == numColumnInRow - 1)
- key.markAsRightEdge(this);
- }
+ public void markAsEdgeKey(final Key key, final int pos) {
+ final int row = mRowNumbers[pos];
+ if (row == 0)
+ key.markAsBottomEdge(this);
+ if (row == mNumRows - 1)
+ key.markAsTopEdge(this);
+
+ final int numColumnInRow = mNumColumnsInRow[row];
+ final int column = getColumnNumber(pos);
+ if (column == 0)
+ key.markAsLeftEdge(this);
+ if (column == numColumnInRow - 1)
+ key.markAsRightEdge(this);
}
+ }
- public Builder(MoreSuggestionsView paneView) {
+ public static final class Builder extends KeyboardBuilder<MoreSuggestionsParam> {
+ private final MoreSuggestionsView mPaneView;
+ private SuggestedWords mSuggestions;
+ private int mFromPos;
+ private int mToPos;
+
+ public Builder(final MoreSuggestionsView paneView) {
super(paneView.getContext(), new MoreSuggestionsParam());
mPaneView = paneView;
}
- public Builder layout(SuggestedWords suggestions, int fromPos, int maxWidth,
- int minWidth, int maxRow) {
+ public Builder layout(final SuggestedWords suggestions, final int fromPos,
+ final int maxWidth, final int minWidth, final int maxRow) {
final Keyboard keyboard = KeyboardSwitcher.getInstance().getKeyboard();
final int xmlId = R.xml.kbd_suggestions_pane_template;
load(xmlId, keyboard.mId);
mParams.mVerticalGap = mParams.mTopPadding = keyboard.mVerticalGap / 2;
+ mPaneView.updateKeyboardGeometry(mParams.mDefaultRowHeight);
final int count = mParams.layout(suggestions, fromPos, maxWidth, minWidth, maxRow,
mPaneView);
mFromPos = fromPos;
@@ -183,25 +191,6 @@ public class MoreSuggestions extends Keyboard {
return this;
}
- private static class Divider extends Key.Spacer {
- private final Drawable mIcon;
-
- public Divider(Keyboard.Params params, Drawable icon, int x, int y, int width,
- int height) {
- super(params, x, y, width, height);
- mIcon = icon;
- }
-
- @Override
- public Drawable getIcon(KeyboardIconsSet iconSet, int alpha) {
- // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the
- // constructor.
- // TODO: Drawable itself should have an alpha value.
- mIcon.setAlpha(128);
- return mIcon;
- }
- }
-
@Override
public MoreSuggestions build() {
final MoreSuggestionsParam params = mParams;
@@ -228,4 +217,23 @@ public class MoreSuggestions extends Keyboard {
return new MoreSuggestions(params);
}
}
+
+ private static final class Divider extends Key.Spacer {
+ private final Drawable mIcon;
+
+ public Divider(final KeyboardParams params, final Drawable icon, final int x,
+ final int y, final int width, final int height) {
+ super(params, x, y, width, height);
+ mIcon = icon;
+ }
+
+ @Override
+ public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) {
+ // KeyboardIconsSet and alpha are unused. Use the icon that has been passed to the
+ // constructor.
+ // TODO: Drawable itself should have an alpha value.
+ mIcon.setAlpha(128);
+ return mIcon;
+ }
+ }
}
diff --git a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
index 19287e3f3..03a2e73d1 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/MoreSuggestionsView.java
@@ -40,7 +40,7 @@ import com.android.inputmethod.latin.R;
* A view that renders a virtual {@link MoreSuggestions}. It handles rendering of keys and detecting
* key presses and touch movements.
*/
-public class MoreSuggestionsView extends KeyboardView implements MoreKeysPanel {
+public final class MoreSuggestionsView extends KeyboardView implements MoreKeysPanel {
private final int[] mCoordinates = new int[2];
final KeyDetector mModalPanelKeyDetector;
@@ -68,7 +68,7 @@ public class MoreSuggestionsView extends KeyboardView implements MoreKeysPanel {
@Override
public void onCodeInput(int primaryCode, int x, int y) {
final int index = primaryCode - MoreSuggestions.SUGGESTION_CODE_BASE;
- if (index >= 0 && index < SuggestionsView.MAX_SUGGESTIONS) {
+ if (index >= 0 && index < SuggestionStripView.MAX_SUGGESTIONS) {
mListener.onCustomRequest(index);
}
}
@@ -105,6 +105,10 @@ public class MoreSuggestionsView extends KeyboardView implements MoreKeysPanel {
}
}
+ public void updateKeyboardGeometry(final int keyHeight) {
+ mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes);
+ }
+
@Override
public void setKeyboard(Keyboard keyboard) {
super.setKeyboard(keyboard);
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index e86390b11..e926fa209 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionsView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -57,22 +57,24 @@ import com.android.inputmethod.keyboard.KeyboardView;
import com.android.inputmethod.keyboard.MoreKeysPanel;
import com.android.inputmethod.keyboard.PointerTracker;
import com.android.inputmethod.keyboard.ViewLayoutUtils;
+import com.android.inputmethod.latin.AutoCorrection;
+import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResearchLogger;
+import com.android.inputmethod.latin.ResourceUtils;
import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
-import com.android.inputmethod.latin.Suggest;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.Utils;
import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.ResearchLogger;
import java.util.ArrayList;
-public class SuggestionsView extends RelativeLayout implements OnClickListener,
+public final class SuggestionStripView extends RelativeLayout implements OnClickListener,
OnLongClickListener {
public interface Listener {
- public boolean addWordToDictionary(String word);
- public void pickSuggestionManually(int index, CharSequence word, int x, int y);
+ public boolean addWordToUserDictionary(String word);
+ public void pickSuggestionManually(int index, CharSequence word);
}
// The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
@@ -88,9 +90,9 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
private final MoreSuggestions.Builder mMoreSuggestionsBuilder;
private final PopupWindow mMoreSuggestionsWindow;
- private final ArrayList<TextView> mWords = new ArrayList<TextView>();
- private final ArrayList<TextView> mInfos = new ArrayList<TextView>();
- private final ArrayList<View> mDividers = new ArrayList<View>();
+ private final ArrayList<TextView> mWords = CollectionUtils.newArrayList();
+ private final ArrayList<TextView> mInfos = CollectionUtils.newArrayList();
+ private final ArrayList<View> mDividers = CollectionUtils.newArrayList();
private final PopupWindow mPreviewPopup;
private final TextView mPreviewText;
@@ -98,24 +100,24 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
private Listener mListener;
private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
- private final SuggestionsViewParams mParams;
+ private final SuggestionStripViewParams mParams;
private static final float MIN_TEXT_XSCALE = 0.70f;
private final UiHandler mHandler = new UiHandler(this);
- private static class UiHandler extends StaticInnerHandlerWrapper<SuggestionsView> {
+ private static final class UiHandler extends StaticInnerHandlerWrapper<SuggestionStripView> {
private static final int MSG_HIDE_PREVIEW = 0;
- public UiHandler(SuggestionsView outerInstance) {
+ public UiHandler(SuggestionStripView outerInstance) {
super(outerInstance);
}
@Override
public void dispatchMessage(Message msg) {
- final SuggestionsView suggestionsView = getOuterInstance();
+ final SuggestionStripView suggestionStripView = getOuterInstance();
switch (msg.what) {
case MSG_HIDE_PREVIEW:
- suggestionsView.hidePreview();
+ suggestionStripView.hidePreview();
break;
}
}
@@ -129,9 +131,9 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
}
}
- private static class SuggestionsViewParams {
+ private static final class SuggestionStripViewParams {
private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
- private static final int DEFAULT_CENTER_SUGGESTION_PERCENTILE = 40;
+ private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f;
private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2;
private static final int PUNCTUATIONS_IN_STRIP = 5;
@@ -167,7 +169,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
private final int mSuggestionStripOption;
- private final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
+ private final ArrayList<CharSequence> mTexts = CollectionUtils.newArrayList();
public boolean mMoreSuggestionsAvailable;
@@ -175,7 +177,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
private final TextView mLeftwardsArrowView;
private final TextView mHintToSaveView;
- public SuggestionsViewParams(Context context, AttributeSet attrs, int defStyle,
+ public SuggestionStripViewParams(Context context, AttributeSet attrs, int defStyle,
ArrayList<TextView> words, ArrayList<View> dividers, ArrayList<TextView> infos) {
mWords = words;
mDividers = dividers;
@@ -191,38 +193,39 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
final Resources res = word.getResources();
mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
- final TypedArray a = context.obtainStyledAttributes(
- attrs, R.styleable.SuggestionsView, defStyle, R.style.SuggestionsViewStyle);
- mSuggestionStripOption = a.getInt(R.styleable.SuggestionsView_suggestionStripOption, 0);
- final float alphaValidTypedWord = getPercent(a,
- R.styleable.SuggestionsView_alphaValidTypedWord, 100);
- final float alphaTypedWord = getPercent(a,
- R.styleable.SuggestionsView_alphaTypedWord, 100);
- final float alphaAutoCorrect = getPercent(a,
- R.styleable.SuggestionsView_alphaAutoCorrect, 100);
- final float alphaSuggested = getPercent(a,
- R.styleable.SuggestionsView_alphaSuggested, 100);
- mAlphaObsoleted = getPercent(a, R.styleable.SuggestionsView_alphaSuggested, 100);
- mColorValidTypedWord = applyAlpha(
- a.getColor(R.styleable.SuggestionsView_colorValidTypedWord, 0),
- alphaValidTypedWord);
- mColorTypedWord = applyAlpha(
- a.getColor(R.styleable.SuggestionsView_colorTypedWord, 0), alphaTypedWord);
- mColorAutoCorrect = applyAlpha(
- a.getColor(R.styleable.SuggestionsView_colorAutoCorrect, 0), alphaAutoCorrect);
- mColorSuggested = applyAlpha(
- a.getColor(R.styleable.SuggestionsView_colorSuggested, 0), alphaSuggested);
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle);
+ 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);
mSuggestionsCountInStrip = a.getInt(
- R.styleable.SuggestionsView_suggestionsCountInStrip,
+ R.styleable.SuggestionStripView_suggestionsCountInStrip,
DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
- mCenterSuggestionWeight = getPercent(a,
- R.styleable.SuggestionsView_centerSuggestionPercentile,
+ mCenterSuggestionWeight = ResourceUtils.getFraction(a,
+ R.styleable.SuggestionStripView_centerSuggestionPercentile,
DEFAULT_CENTER_SUGGESTION_PERCENTILE);
mMaxMoreSuggestionsRow = a.getInt(
- R.styleable.SuggestionsView_maxMoreSuggestionsRow,
+ R.styleable.SuggestionStripView_maxMoreSuggestionsRow,
DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
- mMinMoreSuggestionsWidth = getRatio(a,
- R.styleable.SuggestionsView_minMoreSuggestionsWidth);
+ mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a,
+ R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f);
a.recycle();
mMoreSuggestionsHint = getMoreSuggestionsHint(res,
@@ -276,16 +279,6 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
return new BitmapDrawable(res, buffer);
}
- // Read integer value in TypedArray as percent.
- private static float getPercent(TypedArray a, int index, int defValue) {
- return a.getInt(index, defValue) / 100.0f;
- }
-
- // Read fraction value in TypedArray as float.
- private static float getRatio(TypedArray a, int index) {
- return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
- }
-
private CharSequence getStyledSuggestionWord(SuggestedWords suggestedWords, int pos) {
final CharSequence word = suggestedWords.getWord(pos);
final boolean isAutoCorrect = pos == 1 && suggestedWords.willAutoCorrect();
@@ -336,8 +329,8 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
if (LatinImeLogger.sDBG && suggestedWords.size() > 1) {
// If we auto-correct, then the autocorrection is in slot 0 and the typed word
// is in slot 1.
- if (index == mCenterSuggestionIndex && suggestedWords.mHasAutoCorrectionCandidate
- && Suggest.shouldBlockAutoCorrectionBySafetyNet(
+ if (index == mCenterSuggestionIndex
+ && AutoCorrection.shouldBlockAutoCorrectionBySafetyNet(
suggestedWords.getWord(1).toString(), suggestedWords.getWord(0))) {
return 0xFFFF0000;
}
@@ -596,15 +589,15 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
}
/**
- * Construct a {@link SuggestionsView} for showing suggestions to be picked by the user.
+ * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user.
* @param context
* @param attrs
*/
- public SuggestionsView(Context context, AttributeSet attrs) {
- this(context, attrs, R.attr.suggestionsViewStyle);
+ public SuggestionStripView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.suggestionStripViewStyle);
}
- public SuggestionsView(Context context, AttributeSet attrs, int defStyle) {
+ public SuggestionStripView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final LayoutInflater inflater = LayoutInflater.from(context);
@@ -631,7 +624,8 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
mInfos.add((TextView)inflater.inflate(R.layout.suggestion_info, null));
}
- mParams = new SuggestionsViewParams(context, attrs, defStyle, mWords, mDividers, mInfos);
+ mParams = new SuggestionStripViewParams(
+ context, attrs, defStyle, mWords, mDividers, mInfos);
mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer
@@ -677,7 +671,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
mSuggestedWords = suggestedWords;
mParams.layout(mSuggestedWords, mSuggestionsStrip, this, getWidth());
if (ProductionFlag.IS_EXPERIMENTAL) {
- ResearchLogger.suggestionsView_setSuggestions(mSuggestedWords);
+ ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords);
}
}
@@ -718,19 +712,13 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
mPreviewPopup.dismiss();
}
- private void addToDictionary(CharSequence word) {
- mListener.addWordToDictionary(word.toString());
- }
-
private final KeyboardActionListener mMoreSuggestionsListener =
new KeyboardActionListener.Adapter() {
@Override
public boolean onCustomRequest(int requestCode) {
final int index = requestCode;
final CharSequence word = mSuggestedWords.getWord(index);
- // TODO: change caller path so coordinates are passed through here
- mListener.pickSuggestionManually(index, word, NOT_A_TOUCH_COORDINATE,
- NOT_A_TOUCH_COORDINATE);
+ mListener.pickSuggestionManually(index, word);
dismissMoreSuggestions();
return true;
}
@@ -763,7 +751,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
}
private boolean showMoreSuggestions() {
- final SuggestionsViewParams params = mParams;
+ final SuggestionStripViewParams params = mParams;
if (params.mMoreSuggestionsAvailable) {
final int stripWidth = getWidth();
final View container = mMoreSuggestionsContainer;
@@ -863,7 +851,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
@Override
public void onClick(View view) {
if (mParams.isAddToDictionaryShowing(view)) {
- addToDictionary(mParams.getAddToDictionaryWord());
+ mListener.addWordToUserDictionary(mParams.getAddToDictionaryWord().toString());
clear();
return;
}
@@ -876,7 +864,7 @@ public class SuggestionsView extends RelativeLayout implements OnClickListener,
return;
final CharSequence word = mSuggestedWords.getWord(index);
- mListener.pickSuggestionManually(index, word, mLastX, mLastY);
+ mListener.pickSuggestionManually(index, word);
}
@Override
diff --git a/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java
new file mode 100644
index 000000000..5124a35a6
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/BootBroadcastReceiver.java
@@ -0,0 +1,33 @@
+/*
+ * 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.research;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Arrange for the uploading service to be run on regular intervals.
+ */
+public final class BootBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
+ ResearchLogger.scheduleUploadingService(context);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java
new file mode 100644
index 000000000..11eae8813
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java
@@ -0,0 +1,54 @@
+/*
+ * 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.research;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.CheckBox;
+
+import com.android.inputmethod.latin.R;
+
+public class FeedbackActivity extends Activity {
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.research_feedback_activity);
+ final FeedbackLayout layout = (FeedbackLayout) findViewById(R.id.research_feedback_layout);
+ final CheckBox checkbox = (CheckBox) findViewById(R.id.research_feedback_include_history);
+ final CharSequence cs = checkbox.getText();
+ final String actualString = String.format(cs.toString(),
+ ResearchLogger.FEEDBACK_WORD_BUFFER_SIZE);
+ checkbox.setText(actualString);
+ layout.setActivity(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ public void onBackPressed() {
+ ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
+ super.onBackPressed();
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java
new file mode 100644
index 000000000..a2e08e2b7
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/FeedbackFragment.java
@@ -0,0 +1,73 @@
+/*
+ * 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.research;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.os.Bundle;
+import android.text.Editable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+
+import com.android.inputmethod.latin.R;
+
+public class FeedbackFragment extends Fragment {
+ private EditText mEditText;
+ private CheckBox mCheckBox;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.research_feedback_fragment_layout, container,
+ false);
+ mEditText = (EditText) view.findViewById(R.id.research_feedback_contents);
+ mCheckBox = (CheckBox) view.findViewById(R.id.research_feedback_include_history);
+
+ final Button sendButton = (Button) view.findViewById(
+ R.id.research_feedback_send_button);
+ sendButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Editable editable = mEditText.getText();
+ final String feedbackContents = editable.toString();
+ final boolean includeHistory = mCheckBox.isChecked();
+ ResearchLogger.getInstance().sendFeedback(feedbackContents, includeHistory);
+ final Activity activity = FeedbackFragment.this.getActivity();
+ activity.finish();
+ ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
+ }
+ });
+
+ final Button cancelButton = (Button) view.findViewById(
+ R.id.research_feedback_cancel_button);
+ cancelButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Activity activity = FeedbackFragment.this.getActivity();
+ activity.finish();
+ ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
+ }
+ });
+
+ return view;
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/FeedbackLayout.java b/java/src/com/android/inputmethod/research/FeedbackLayout.java
new file mode 100644
index 000000000..f2cbfe308
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/FeedbackLayout.java
@@ -0,0 +1,62 @@
+/*
+ * 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.research;
+
+import android.app.Activity;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.LinearLayout;
+
+public class FeedbackLayout extends LinearLayout {
+ private Activity mActivity;
+
+ public FeedbackLayout(Context context) {
+ super(context);
+ }
+
+ public FeedbackLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public FeedbackLayout(Context context, AttributeSet attrs, int defstyle) {
+ super(context, attrs, defstyle);
+ }
+
+ public void setActivity(Activity activity) {
+ mActivity = activity;
+ }
+
+ @Override
+ public boolean dispatchKeyEventPreIme(KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ KeyEvent.DispatcherState state = getKeyDispatcherState();
+ if (state != null) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getRepeatCount() == 0) {
+ state.startTracking(event, this);
+ return true;
+ } else if (event.getAction() == KeyEvent.ACTION_UP
+ && !event.isCanceled() && state.isTracking(event)) {
+ mActivity.onBackPressed();
+ return true;
+ }
+ }
+ }
+ return super.dispatchKeyEventPreIme(event);
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/LogBuffer.java b/java/src/com/android/inputmethod/research/LogBuffer.java
new file mode 100644
index 000000000..ae7b1579a
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/LogBuffer.java
@@ -0,0 +1,113 @@
+/*
+ * 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.research;
+
+import com.android.inputmethod.latin.CollectionUtils;
+
+import java.util.LinkedList;
+
+/**
+ * A buffer that holds a fixed number of LogUnits.
+ *
+ * LogUnits are added in and shifted out in temporal order. Only a subset of the LogUnits are
+ * actual words; the other LogUnits do not count toward the word limit. Once the buffer reaches
+ * capacity, adding another LogUnit that is a word evicts the oldest LogUnits out one at a time to
+ * stay under the capacity limit.
+ */
+public class LogBuffer {
+ protected final LinkedList<LogUnit> mLogUnits;
+ /* package for test */ int mWordCapacity;
+ // The number of members of mLogUnits that are actual words.
+ protected int mNumActualWords;
+
+ /**
+ * Create a new LogBuffer that can hold a fixed number of LogUnits that are words (and
+ * unlimited number of non-word LogUnits), and that outputs its result to a researchLog.
+ *
+ * @param wordCapacity maximum number of words
+ */
+ LogBuffer(final int wordCapacity) {
+ if (wordCapacity <= 0) {
+ throw new IllegalArgumentException("wordCapacity must be 1 or greater.");
+ }
+ mLogUnits = CollectionUtils.newLinkedList();
+ mWordCapacity = wordCapacity;
+ mNumActualWords = 0;
+ }
+
+ /**
+ * Adds a new LogUnit to the front of the LIFO queue, evicting existing LogUnit's
+ * (oldest first) if word capacity is reached.
+ */
+ public void shiftIn(LogUnit newLogUnit) {
+ if (newLogUnit.getWord() == null) {
+ // This LogUnit isn't a word, so it doesn't count toward the word-limit.
+ mLogUnits.add(newLogUnit);
+ return;
+ }
+ if (mNumActualWords == mWordCapacity) {
+ shiftOutThroughFirstWord();
+ }
+ mLogUnits.add(newLogUnit);
+ mNumActualWords++; // Must be a word, or we wouldn't be here.
+ }
+
+ private void shiftOutThroughFirstWord() {
+ while (!mLogUnits.isEmpty()) {
+ final LogUnit logUnit = mLogUnits.removeFirst();
+ onShiftOut(logUnit);
+ if (logUnit.hasWord()) {
+ // Successfully shifted out a word-containing LogUnit and made space for the new
+ // LogUnit.
+ mNumActualWords--;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Removes all LogUnits from the buffer without calling onShiftOut().
+ */
+ public void clear() {
+ mLogUnits.clear();
+ mNumActualWords = 0;
+ }
+
+ /**
+ * Called when a LogUnit is removed from the LogBuffer as a result of a shiftIn. LogUnits are
+ * removed in the order entered. This method is not called when shiftOut is called directly.
+ *
+ * Base class does nothing; subclasses may override.
+ */
+ protected void onShiftOut(LogUnit logUnit) {
+ }
+
+ /**
+ * Called to deliberately remove the oldest LogUnit. Usually called when draining the
+ * LogBuffer.
+ */
+ public LogUnit shiftOut() {
+ if (mLogUnits.isEmpty()) {
+ return null;
+ }
+ final LogUnit logUnit = mLogUnits.removeFirst();
+ if (logUnit.hasWord()) {
+ mNumActualWords--;
+ }
+ return logUnit;
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
new file mode 100644
index 000000000..d8b3a29ff
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -0,0 +1,83 @@
+/*
+ * 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.research;
+
+import com.android.inputmethod.latin.CollectionUtils;
+
+import java.util.ArrayList;
+
+/**
+ * A group of log statements related to each other.
+ *
+ * A LogUnit is collection of LogStatements, each of which is generated by at a particular point
+ * in the code. (There is no LogStatement class; the data is stored across the instance variables
+ * here.) A single LogUnit's statements can correspond to all the calls made while in the same
+ * composing region, or all the calls between committing the last composing region, and the first
+ * character of the next composing region.
+ *
+ * Individual statements in a log may be marked as potentially private. If so, then they are only
+ * published to a ResearchLog if the ResearchLogger determines that publishing the entire LogUnit
+ * will not violate the user's privacy. Checks for this may include whether other LogUnits have
+ * been published recently, or whether the LogUnit contains numbers, etc.
+ */
+/* package */ class LogUnit {
+ private final ArrayList<String[]> mKeysList = CollectionUtils.newArrayList();
+ private final ArrayList<Object[]> mValuesList = CollectionUtils.newArrayList();
+ private final ArrayList<Boolean> mIsPotentiallyPrivate = CollectionUtils.newArrayList();
+ private String mWord;
+ private boolean mContainsDigit;
+
+ public void addLogStatement(final String[] keys, final Object[] values,
+ final Boolean isPotentiallyPrivate) {
+ mKeysList.add(keys);
+ mValuesList.add(values);
+ mIsPotentiallyPrivate.add(isPotentiallyPrivate);
+ }
+
+ public void publishTo(final ResearchLog researchLog, final boolean isIncludingPrivateData) {
+ final int size = mKeysList.size();
+ for (int i = 0; i < size; i++) {
+ if (!mIsPotentiallyPrivate.get(i) || isIncludingPrivateData) {
+ researchLog.outputEvent(mKeysList.get(i), mValuesList.get(i));
+ }
+ }
+ }
+
+ public void setWord(String word) {
+ mWord = word;
+ }
+
+ public String getWord() {
+ return mWord;
+ }
+
+ public boolean hasWord() {
+ return mWord != null;
+ }
+
+ public void setContainsDigit() {
+ mContainsDigit = true;
+ }
+
+ public boolean hasDigit() {
+ return mContainsDigit;
+ }
+
+ public boolean isEmpty() {
+ return mKeysList.isEmpty();
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
new file mode 100644
index 000000000..745768d35
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java
@@ -0,0 +1,127 @@
+/*
+ * 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.research;
+
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.Suggest;
+
+import java.util.Random;
+
+public class MainLogBuffer extends LogBuffer {
+ // The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams.
+ private static final int N_GRAM_SIZE = 2;
+ // The number of words between n-grams to omit from the log.
+ private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES = 18;
+
+ private final ResearchLog mResearchLog;
+ private Suggest mSuggest;
+
+ // The minimum periodicity with which n-grams can be sampled. E.g. mWinWordPeriod is 10 if
+ // every 10th bigram is sampled, i.e., words 1-8 are not, but the bigram at words 9 and 10, etc.
+ // for 11-18, and the bigram at words 19 and 20. If an n-gram is not safe (e.g. it contains a
+ // number in the middle or an out-of-vocabulary word), then sampling is delayed until a safe
+ // n-gram does appear.
+ /* package for test */ int mMinWordPeriod;
+
+ // Counter for words left to suppress before an n-gram can be sampled. Reset to mMinWordPeriod
+ // after a sample is taken.
+ /* package for test */ int mWordsUntilSafeToSample;
+
+ public MainLogBuffer(final ResearchLog researchLog) {
+ super(N_GRAM_SIZE);
+ mResearchLog = researchLog;
+ mMinWordPeriod = DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES + N_GRAM_SIZE;
+ final Random random = new Random();
+ mWordsUntilSafeToSample = random.nextInt(mMinWordPeriod);
+ }
+
+ public void setSuggest(Suggest suggest) {
+ mSuggest = suggest;
+ }
+
+ @Override
+ public void shiftIn(final LogUnit newLogUnit) {
+ super.shiftIn(newLogUnit);
+ if (newLogUnit.hasWord()) {
+ if (mWordsUntilSafeToSample > 0) {
+ mWordsUntilSafeToSample--;
+ }
+ }
+ }
+
+ public void resetWordCounter() {
+ mWordsUntilSafeToSample = mMinWordPeriod;
+ }
+
+ /**
+ * Determines whether the content of the MainLogBuffer can be safely uploaded in its complete
+ * form and still protect the user's privacy.
+ *
+ * The size of the MainLogBuffer is just enough to hold one n-gram, its corrections, and any
+ * non-character data that is typed between words. The decision about privacy is made based on
+ * the buffer's entire content. If it is decided that the privacy risks are too great to upload
+ * the contents of this buffer, a censored version of the LogItems may still be uploaded. E.g.,
+ * the screen orientation and other characteristics about the device can be uploaded without
+ * revealing much about the user.
+ */
+ public boolean isSafeToLog() {
+ // Check that we are not sampling too frequently. Having sampled recently might disclose
+ // too much of the user's intended meaning.
+ if (mWordsUntilSafeToSample > 0) {
+ return false;
+ }
+ if (mSuggest == null || !mSuggest.hasMainDictionary()) {
+ // Main dictionary is unavailable. Since we cannot check it, we cannot tell if a word
+ // is out-of-vocabulary or not. Therefore, we must judge the entire buffer contents to
+ // potentially pose a privacy risk.
+ return false;
+ }
+ // Reload the dictionary in case it has changed (e.g., because the user has changed
+ // languages).
+ final Dictionary dictionary = mSuggest.getMainDictionary();
+ if (dictionary == null) {
+ return false;
+ }
+ // Check each word in the buffer. If any word poses a privacy threat, we cannot upload the
+ // complete buffer contents in detail.
+ final int length = mLogUnits.size();
+ for (int i = 0; i < length; i++) {
+ final LogUnit logUnit = mLogUnits.get(i);
+ final String word = logUnit.getWord();
+ if (word == null) {
+ // Digits outside words are a privacy threat.
+ if (logUnit.hasDigit()) {
+ return false;
+ }
+ } else {
+ // Words not in the dictionary are a privacy threat.
+ if (!(dictionary.isValidWord(word))) {
+ return false;
+ }
+ }
+ }
+ // All checks have passed; this buffer's content can be safely uploaded.
+ return true;
+ }
+
+ @Override
+ protected void onShiftOut(LogUnit logUnit) {
+ if (mResearchLog != null) {
+ mResearchLog.publish(logUnit, false /* isIncludingPrivateData */);
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java
new file mode 100644
index 000000000..70c38e909
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/ResearchLog.java
@@ -0,0 +1,320 @@
+/*
+ * 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.research;
+
+import android.content.SharedPreferences;
+import android.os.SystemClock;
+import android.util.JsonWriter;
+import android.util.Log;
+import android.view.inputmethod.CompletionInfo;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Logs the use of the LatinIME keyboard.
+ *
+ * This class logs operations on the IME keyboard, including what the user has typed.
+ * Data is stored locally in a file in app-specific storage.
+ *
+ * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
+ */
+public class ResearchLog {
+ private static final String TAG = ResearchLog.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ private static final long FLUSH_DELAY_IN_MS = 1000 * 5;
+ private static final int ABORT_TIMEOUT_IN_MS = 1000 * 4;
+
+ /* package */ final ScheduledExecutorService mExecutor;
+ /* package */ final File mFile;
+ private JsonWriter mJsonWriter = NULL_JSON_WRITER;
+ // true if at least one byte of data has been written out to the log file. This must be
+ // remembered because JsonWriter requires that calls matching calls to beginObject and
+ // endObject, as well as beginArray and endArray, and the file is opened lazily, only when
+ // it is certain that data will be written. Alternatively, the matching call exceptions
+ // could be caught, but this might suppress other errors.
+ private boolean mHasWrittenData = false;
+
+ private static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
+ new OutputStreamWriter(new NullOutputStream()));
+ private static class NullOutputStream extends OutputStream {
+ /** {@inheritDoc} */
+ @Override
+ public void write(byte[] buffer, int offset, int count) {
+ // nop
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void write(byte[] buffer) {
+ // nop
+ }
+
+ @Override
+ public void write(int oneByte) {
+ }
+ }
+
+ public ResearchLog(final File outputFile) {
+ if (outputFile == null) {
+ throw new IllegalArgumentException();
+ }
+ mExecutor = Executors.newSingleThreadScheduledExecutor();
+ mFile = outputFile;
+ }
+
+ public synchronized void close(final Runnable onClosed) {
+ mExecutor.submit(new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ try {
+ if (mHasWrittenData) {
+ mJsonWriter.endArray();
+ mJsonWriter.flush();
+ mJsonWriter.close();
+ if (DEBUG) {
+ Log.d(TAG, "wrote log to " + mFile);
+ }
+ mHasWrittenData = false;
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "close() called, but no data, not outputting");
+ }
+ }
+ } catch (Exception e) {
+ Log.d(TAG, "error when closing ResearchLog:");
+ e.printStackTrace();
+ } finally {
+ if (mFile.exists()) {
+ mFile.setWritable(false, false);
+ }
+ if (onClosed != null) {
+ onClosed.run();
+ }
+ }
+ return null;
+ }
+ });
+ removeAnyScheduledFlush();
+ mExecutor.shutdown();
+ }
+
+ private boolean mIsAbortSuccessful;
+
+ public synchronized void abort() {
+ mExecutor.submit(new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ try {
+ if (mHasWrittenData) {
+ mJsonWriter.endArray();
+ mJsonWriter.close();
+ mHasWrittenData = false;
+ }
+ } finally {
+ mIsAbortSuccessful = mFile.delete();
+ }
+ return null;
+ }
+ });
+ removeAnyScheduledFlush();
+ mExecutor.shutdown();
+ }
+
+ public boolean blockingAbort() throws InterruptedException {
+ abort();
+ mExecutor.awaitTermination(ABORT_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
+ return mIsAbortSuccessful;
+ }
+
+ public void awaitTermination(int delay, TimeUnit timeUnit) throws InterruptedException {
+ mExecutor.awaitTermination(delay, timeUnit);
+ }
+
+ /* package */ synchronized void flush() {
+ removeAnyScheduledFlush();
+ mExecutor.submit(mFlushCallable);
+ }
+
+ private final Callable<Object> mFlushCallable = new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ mJsonWriter.flush();
+ return null;
+ }
+ };
+
+ private ScheduledFuture<Object> mFlushFuture;
+
+ private void removeAnyScheduledFlush() {
+ if (mFlushFuture != null) {
+ mFlushFuture.cancel(false);
+ mFlushFuture = null;
+ }
+ }
+
+ private void scheduleFlush() {
+ removeAnyScheduledFlush();
+ mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS);
+ }
+
+ public synchronized void publish(final LogUnit logUnit, final boolean isIncludingPrivateData) {
+ try {
+ mExecutor.submit(new Callable<Object>() {
+ @Override
+ public Object call() throws Exception {
+ logUnit.publishTo(ResearchLog.this, isIncludingPrivateData);
+ scheduleFlush();
+ return null;
+ }
+ });
+ } catch (RejectedExecutionException e) {
+ // TODO: Add code to record loss of data, and report.
+ }
+ }
+
+ private static final String CURRENT_TIME_KEY = "_ct";
+ private static final String UPTIME_KEY = "_ut";
+ private static final String EVENT_TYPE_KEY = "_ty";
+
+ void outputEvent(final String[] keys, final Object[] values) {
+ // Not thread safe.
+ if (keys.length == 0) {
+ return;
+ }
+ if (DEBUG) {
+ if (keys.length != values.length + 1) {
+ Log.d(TAG, "Key and Value list sizes do not match. " + keys[0]);
+ }
+ }
+ try {
+ if (mJsonWriter == NULL_JSON_WRITER) {
+ mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile)));
+ mJsonWriter.beginArray();
+ mHasWrittenData = true;
+ }
+ mJsonWriter.beginObject();
+ mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
+ mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis());
+ mJsonWriter.name(EVENT_TYPE_KEY).value(keys[0]);
+ final int length = values.length;
+ for (int i = 0; i < length; i++) {
+ mJsonWriter.name(keys[i + 1]);
+ Object value = values[i];
+ if (value instanceof CharSequence) {
+ mJsonWriter.value(value.toString());
+ } else if (value instanceof Number) {
+ mJsonWriter.value((Number) value);
+ } else if (value instanceof Boolean) {
+ mJsonWriter.value((Boolean) value);
+ } else if (value instanceof CompletionInfo[]) {
+ CompletionInfo[] ci = (CompletionInfo[]) value;
+ mJsonWriter.beginArray();
+ for (int j = 0; j < ci.length; j++) {
+ mJsonWriter.value(ci[j].toString());
+ }
+ mJsonWriter.endArray();
+ } else if (value instanceof SharedPreferences) {
+ SharedPreferences prefs = (SharedPreferences) value;
+ mJsonWriter.beginObject();
+ for (Map.Entry<String,?> entry : prefs.getAll().entrySet()) {
+ mJsonWriter.name(entry.getKey());
+ final Object innerValue = entry.getValue();
+ if (innerValue == null) {
+ mJsonWriter.nullValue();
+ } else if (innerValue instanceof Boolean) {
+ mJsonWriter.value((Boolean) innerValue);
+ } else if (innerValue instanceof Number) {
+ mJsonWriter.value((Number) innerValue);
+ } else {
+ mJsonWriter.value(innerValue.toString());
+ }
+ }
+ mJsonWriter.endObject();
+ } else if (value instanceof Key[]) {
+ Key[] keyboardKeys = (Key[]) value;
+ mJsonWriter.beginArray();
+ for (Key keyboardKey : keyboardKeys) {
+ mJsonWriter.beginObject();
+ mJsonWriter.name("code").value(keyboardKey.mCode);
+ mJsonWriter.name("altCode").value(keyboardKey.getAltCode());
+ mJsonWriter.name("x").value(keyboardKey.mX);
+ mJsonWriter.name("y").value(keyboardKey.mY);
+ mJsonWriter.name("w").value(keyboardKey.mWidth);
+ mJsonWriter.name("h").value(keyboardKey.mHeight);
+ mJsonWriter.endObject();
+ }
+ mJsonWriter.endArray();
+ } else if (value instanceof SuggestedWords) {
+ SuggestedWords words = (SuggestedWords) value;
+ mJsonWriter.beginObject();
+ mJsonWriter.name("typedWordValid").value(words.mTypedWordValid);
+ mJsonWriter.name("willAutoCorrect").value(words.mWillAutoCorrect);
+ mJsonWriter.name("isPunctuationSuggestions")
+ .value(words.mIsPunctuationSuggestions);
+ mJsonWriter.name("isObsoleteSuggestions").value(words.mIsObsoleteSuggestions);
+ mJsonWriter.name("isPrediction").value(words.mIsPrediction);
+ mJsonWriter.name("words");
+ mJsonWriter.beginArray();
+ final int size = words.size();
+ for (int j = 0; j < size; j++) {
+ SuggestedWordInfo wordInfo = words.getWordInfo(j);
+ mJsonWriter.value(wordInfo.toString());
+ }
+ mJsonWriter.endArray();
+ mJsonWriter.endObject();
+ } else if (value == null) {
+ mJsonWriter.nullValue();
+ } else {
+ Log.w(TAG, "Unrecognized type to be logged: " +
+ (value == null ? "<null>" : value.getClass().getName()));
+ mJsonWriter.nullValue();
+ }
+ }
+ mJsonWriter.endObject();
+ } catch (IOException e) {
+ e.printStackTrace();
+ Log.w(TAG, "Error in JsonWriter; disabling logging");
+ try {
+ mJsonWriter.close();
+ } catch (IllegalStateException e1) {
+ // Assume that this is just the json not being terminated properly.
+ // Ignore
+ } catch (IOException e1) {
+ e1.printStackTrace();
+ } finally {
+ mJsonWriter = NULL_JSON_WRITER;
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
new file mode 100644
index 000000000..763fd6e00
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -0,0 +1,1315 @@
+/*
+ * 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.research;
+
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+
+import android.app.AlarmManager;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.inputmethodservice.InputMethodService;
+import android.net.Uri;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.SystemClock;
+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.CorrectionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.Toast;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.keyboard.KeyboardView;
+import com.android.inputmethod.keyboard.MainKeyboardView;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.Dictionary;
+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 java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.UUID;
+
+/**
+ * Logs the use of the LatinIME keyboard.
+ *
+ * This class logs operations on the IME keyboard, including what the user has typed.
+ * Data is stored locally in a file in app-specific storage.
+ *
+ * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
+ */
+public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final String TAG = ResearchLogger.class.getSimpleName();
+ private static final boolean DEBUG = false;
+ private static final boolean OUTPUT_ENTIRE_BUFFER = false; // true may disclose private info
+ public static final boolean DEFAULT_USABILITY_STUDY_MODE = false;
+ /* package */ static boolean sIsLogging = false;
+ private static final int OUTPUT_FORMAT_VERSION = 1;
+ private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
+ private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash";
+ /* package */ static final String FILENAME_PREFIX = "researchLog";
+ private static final String FILENAME_SUFFIX = ".txt";
+ private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
+ new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
+ private static final boolean IS_SHOWING_INDICATOR = true;
+ private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false;
+ public static final int FEEDBACK_WORD_BUFFER_SIZE = 5;
+
+ // 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_LOGGER_UUID_STRING = "pref_research_logger_uuid";
+
+ private static final ResearchLogger sInstance = new ResearchLogger();
+ // to write to a different filename, e.g., for testing, set mFile before calling start()
+ /* package */ File mFilesDir;
+ /* package */ String mUUIDString;
+ /* package */ ResearchLog mMainResearchLog;
+ // mFeedbackLog records all events for the session, private or not (excepting
+ // passwords). It is written to permanent storage only if the user explicitly commands
+ // the system to do so.
+ // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
+ // complete.
+ /* package */ ResearchLog mFeedbackLog;
+ /* package */ MainLogBuffer mMainLogBuffer;
+ /* package */ LogBuffer mFeedbackLogBuffer;
+
+ private boolean mIsPasswordView = false;
+ private boolean mIsLoggingSuspended = false;
+ private SharedPreferences mPrefs;
+
+ // digits entered by the user are replaced with this codepoint.
+ /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT =
+ Character.codePointAt("\uE000", 0); // U+E000 is in the "private-use area"
+ // U+E001 is in the "private-use area"
+ /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
+ private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time";
+ private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
+ private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS;
+ protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
+ // set when LatinIME should ignore an onUpdateSelection() callback that
+ // arises from operations in this class
+ private static boolean sLatinIMEExpectingUpdateSelection = false;
+
+ // used to check whether words are not unique
+ private Suggest mSuggest;
+ private Dictionary mDictionary;
+ private MainKeyboardView mMainKeyboardView;
+ private InputMethodService mInputMethodService;
+ private final Statistics mStatistics;
+
+ private Intent mUploadIntent;
+ private PendingIntent mUploadPendingIntent;
+
+ private LogUnit mCurrentLogUnit = new LogUnit();
+
+ private ResearchLogger() {
+ mStatistics = Statistics.getInstance();
+ }
+
+ public static ResearchLogger getInstance() {
+ return sInstance;
+ }
+
+ public void init(final InputMethodService ims, final SharedPreferences prefs) {
+ assert ims != null;
+ if (ims == null) {
+ Log.w(TAG, "IMS is null; logging is off");
+ } else {
+ mFilesDir = ims.getFilesDir();
+ if (mFilesDir == null || !mFilesDir.exists()) {
+ Log.w(TAG, "IME storage directory does not exist.");
+ }
+ }
+ if (prefs != null) {
+ mUUIDString = getUUID(prefs);
+ if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) {
+ Editor e = prefs.edit();
+ e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE);
+ e.apply();
+ }
+ sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+
+ final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L);
+ final long now = System.currentTimeMillis();
+ if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) {
+ final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS;
+ cleanupLoggingDir(mFilesDir, timeHorizon);
+ Editor e = prefs.edit();
+ e.putLong(PREF_LAST_CLEANUP_TIME, now);
+ e.apply();
+ }
+ }
+ mInputMethodService = ims;
+ mPrefs = prefs;
+ mUploadIntent = new Intent(mInputMethodService, UploaderService.class);
+ mUploadPendingIntent = PendingIntent.getService(mInputMethodService, 0, mUploadIntent, 0);
+
+ if (ProductionFlag.IS_EXPERIMENTAL) {
+ scheduleUploadingService(mInputMethodService);
+ }
+ }
+
+ /**
+ * Arrange for the UploaderService to be run on a regular basis.
+ *
+ * Any existing scheduled invocation of UploaderService is removed and rescheduled. This may
+ * cause problems if this method is called often and frequent updates are required, but since
+ * the user will likely be sleeping at some point, if the interval is less that the expected
+ * sleep duration and this method is not called during that time, the service should be invoked
+ * at some point.
+ */
+ public static void scheduleUploadingService(Context context) {
+ final Intent intent = new Intent(context, UploaderService.class);
+ final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
+ final AlarmManager manager =
+ (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ manager.cancel(pendingIntent);
+ manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent);
+ }
+
+ private void cleanupLoggingDir(final File dir, final long time) {
+ for (File file : dir.listFiles()) {
+ if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) &&
+ file.lastModified() < time) {
+ file.delete();
+ }
+ }
+ }
+
+ public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) {
+ mMainKeyboardView = mainKeyboardView;
+ maybeShowSplashScreen();
+ }
+
+ public void mainKeyboardView_onDetachedFromWindow() {
+ mMainKeyboardView = null;
+ }
+
+ private boolean hasSeenSplash() {
+ return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false);
+ }
+
+ private Dialog mSplashDialog = null;
+
+ private void maybeShowSplashScreen() {
+ if (hasSeenSplash()) {
+ 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(mInputMethodService)
+ .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 = mInputMethodService.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);
+ mInputMethodService.startActivity(intent);
+ }
+ })
+ .setCancelable(true)
+ .setOnCancelListener(
+ new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ mInputMethodService.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() {
+ setLoggingAllowed(true);
+ if (mPrefs == null) {
+ return;
+ }
+ final Editor e = mPrefs.edit();
+ e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true);
+ e.apply();
+ restart();
+ }
+
+ private void setLoggingAllowed(boolean enableLogging) {
+ if (mPrefs == null) {
+ return;
+ }
+ Editor e = mPrefs.edit();
+ e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging);
+ e.apply();
+ sIsLogging = enableLogging;
+ }
+
+ private File createLogFile(File filesDir) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(FILENAME_PREFIX).append('-');
+ sb.append(mUUIDString).append('-');
+ sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
+ sb.append(FILENAME_SUFFIX);
+ return new File(filesDir, sb.toString());
+ }
+
+ private void checkForEmptyEditor() {
+ if (mInputMethodService == null) {
+ return;
+ }
+ final InputConnection ic = mInputMethodService.getCurrentInputConnection();
+ if (ic == null) {
+ return;
+ }
+ final CharSequence textBefore = ic.getTextBeforeCursor(1, 0);
+ if (!TextUtils.isEmpty(textBefore)) {
+ mStatistics.setIsEmptyUponStarting(false);
+ return;
+ }
+ final CharSequence textAfter = ic.getTextAfterCursor(1, 0);
+ if (!TextUtils.isEmpty(textAfter)) {
+ mStatistics.setIsEmptyUponStarting(false);
+ return;
+ }
+ if (textBefore != null && textAfter != null) {
+ mStatistics.setIsEmptyUponStarting(true);
+ }
+ }
+
+ private void start() {
+ if (DEBUG) {
+ Log.d(TAG, "start called");
+ }
+ maybeShowSplashScreen();
+ updateSuspendedState();
+ requestIndicatorRedraw();
+ mStatistics.reset();
+ checkForEmptyEditor();
+ if (!isAllowedToLog()) {
+ // Log.w(TAG, "not in usability mode; not logging");
+ return;
+ }
+ if (mFilesDir == null || !mFilesDir.exists()) {
+ Log.w(TAG, "IME storage directory does not exist. Cannot start logging.");
+ return;
+ }
+ if (mMainLogBuffer == null) {
+ mMainResearchLog = new ResearchLog(createLogFile(mFilesDir));
+ mMainLogBuffer = new MainLogBuffer(mMainResearchLog);
+ mMainLogBuffer.setSuggest(mSuggest);
+ }
+ if (mFeedbackLogBuffer == null) {
+ mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
+ // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold
+ // the feedback LogUnit itself.
+ mFeedbackLogBuffer = new LogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1);
+ }
+ }
+
+ /* package */ void stop() {
+ if (DEBUG) {
+ Log.d(TAG, "stop called");
+ }
+ logStatistics();
+ commitCurrentLogUnit();
+
+ if (mMainLogBuffer != null) {
+ publishLogBuffer(mMainLogBuffer, mMainResearchLog, false /* isIncludingPrivateData */);
+ mMainResearchLog.close(null /* callback */);
+ mMainLogBuffer = null;
+ }
+ if (mFeedbackLogBuffer != null) {
+ mFeedbackLog.close(null /* callback */);
+ mFeedbackLogBuffer = null;
+ }
+ }
+
+ public boolean abort() {
+ if (DEBUG) {
+ Log.d(TAG, "abort called");
+ }
+ boolean didAbortMainLog = false;
+ if (mMainLogBuffer != null) {
+ mMainLogBuffer.clear();
+ try {
+ didAbortMainLog = mMainResearchLog.blockingAbort();
+ } catch (InterruptedException e) {
+ // Don't know whether this succeeded or not. We assume not; this is reported
+ // to the caller.
+ }
+ mMainLogBuffer = null;
+ }
+ boolean didAbortFeedbackLog = false;
+ if (mFeedbackLogBuffer != null) {
+ mFeedbackLogBuffer.clear();
+ try {
+ didAbortFeedbackLog = mFeedbackLog.blockingAbort();
+ } catch (InterruptedException e) {
+ // Don't know whether this succeeded or not. We assume not; this is reported
+ // to the caller.
+ }
+ mFeedbackLogBuffer = null;
+ }
+ return didAbortMainLog && didAbortFeedbackLog;
+ }
+
+ private void restart() {
+ stop();
+ start();
+ }
+
+ private long mResumeTime = 0L;
+ private void suspendLoggingUntil(long time) {
+ mIsLoggingSuspended = true;
+ mResumeTime = time;
+ requestIndicatorRedraw();
+ }
+
+ private void resumeLogging() {
+ mResumeTime = 0L;
+ updateSuspendedState();
+ requestIndicatorRedraw();
+ if (isAllowedToLog()) {
+ restart();
+ }
+ }
+
+ private void updateSuspendedState() {
+ final long time = System.currentTimeMillis();
+ if (time > mResumeTime) {
+ mIsLoggingSuspended = false;
+ }
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+ if (key == null || prefs == null) {
+ return;
+ }
+ sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
+ if (sIsLogging == false) {
+ abort();
+ }
+ requestIndicatorRedraw();
+ mPrefs = prefs;
+ prefsChanged(prefs);
+ }
+
+ public void onResearchKeySelected(final LatinIME latinIME) {
+ if (mInFeedbackDialog) {
+ Toast.makeText(latinIME, R.string.research_please_exit_feedback_form,
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+ presentFeedbackDialog(latinIME);
+ }
+
+ // TODO: currently unreachable. Remove after being sure no menu is needed.
+ /*
+ public void presentResearchDialog(final LatinIME latinIME) {
+ final CharSequence title = latinIME.getString(R.string.english_ime_research_log);
+ final boolean showEnable = mIsLoggingSuspended || !sIsLogging;
+ final CharSequence[] items = new CharSequence[] {
+ latinIME.getString(R.string.research_feedback_menu_option),
+ showEnable ? latinIME.getString(R.string.research_enable_session_logging) :
+ latinIME.getString(R.string.research_do_not_log_this_session)
+ };
+ final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface di, int position) {
+ di.dismiss();
+ switch (position) {
+ case 0:
+ presentFeedbackDialog(latinIME);
+ break;
+ case 1:
+ enableOrDisable(showEnable, latinIME);
+ break;
+ }
+ }
+
+ };
+ final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
+ .setItems(items, listener)
+ .setTitle(title);
+ latinIME.showOptionDialog(builder.create());
+ }
+ */
+
+ private boolean mInFeedbackDialog = false;
+ public void presentFeedbackDialog(LatinIME latinIME) {
+ mInFeedbackDialog = true;
+ latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
+ }
+
+ // TODO: currently unreachable. Remove after being sure enable/disable is
+ // not needed.
+ /*
+ public void enableOrDisable(final boolean showEnable, final LatinIME latinIME) {
+ if (showEnable) {
+ if (!sIsLogging) {
+ setLoggingAllowed(true);
+ }
+ resumeLogging();
+ Toast.makeText(latinIME,
+ R.string.research_notify_session_logging_enabled,
+ Toast.LENGTH_LONG).show();
+ } else {
+ Toast toast = Toast.makeText(latinIME,
+ R.string.research_notify_session_log_deleting,
+ Toast.LENGTH_LONG);
+ toast.show();
+ boolean isLogDeleted = abort();
+ final long currentTime = System.currentTimeMillis();
+ final long resumeTime = currentTime + 1000 * 60 *
+ SUSPEND_DURATION_IN_MINUTES;
+ suspendLoggingUntil(resumeTime);
+ toast.cancel();
+ Toast.makeText(latinIME, R.string.research_notify_logging_suspended,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+ */
+
+ private static final String[] EVENTKEYS_FEEDBACK = {
+ "UserTimestamp", "contents"
+ };
+ public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
+ if (mFeedbackLogBuffer == null) {
+ return;
+ }
+ if (includeHistory) {
+ commitCurrentLogUnit();
+ } else {
+ mFeedbackLogBuffer.clear();
+ }
+ final LogUnit feedbackLogUnit = new LogUnit();
+ final Object[] values = {
+ feedbackContents
+ };
+ feedbackLogUnit.addLogStatement(EVENTKEYS_FEEDBACK, values,
+ false /* isPotentiallyPrivate */);
+ mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
+ publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */);
+ mFeedbackLog.close(new Runnable() {
+ @Override
+ public void run() {
+ uploadNow();
+ }
+ });
+ mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
+ }
+
+ public void uploadNow() {
+ if (DEBUG) {
+ Log.d(TAG, "calling uploadNow()");
+ }
+ mInputMethodService.startService(mUploadIntent);
+ }
+
+ public void onLeavingSendFeedbackDialog() {
+ mInFeedbackDialog = false;
+ }
+
+ public void initSuggest(Suggest suggest) {
+ mSuggest = suggest;
+ if (mMainLogBuffer != null) {
+ mMainLogBuffer.setSuggest(mSuggest);
+ }
+ }
+
+ private void setIsPasswordView(boolean isPasswordView) {
+ mIsPasswordView = isPasswordView;
+ }
+
+ private boolean isAllowedToLog() {
+ if (DEBUG) {
+ Log.d(TAG, "iatl: " +
+ "mipw=" + mIsPasswordView +
+ ", mils=" + mIsLoggingSuspended +
+ ", sil=" + sIsLogging +
+ ", mInFeedbackDialog=" + mInFeedbackDialog);
+ }
+ return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
+ }
+
+ public void requestIndicatorRedraw() {
+ if (!IS_SHOWING_INDICATOR) {
+ return;
+ }
+ if (mMainKeyboardView == null) {
+ return;
+ }
+ mMainKeyboardView.invalidateAllKeys();
+ }
+
+
+ public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width,
+ int height) {
+ // TODO: Reimplement using a keyboard background image specific to the ResearchLogger
+ // and remove this method.
+ // The check for MainKeyboardView ensures that a red border is only placed around
+ // the main keyboard, not every keyboard.
+ if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) {
+ final int savedColor = paint.getColor();
+ paint.setColor(Color.RED);
+ final Style savedStyle = paint.getStyle();
+ paint.setStyle(Style.STROKE);
+ final float savedStrokeWidth = paint.getStrokeWidth();
+ if (IS_SHOWING_INDICATOR_CLEARLY) {
+ paint.setStrokeWidth(5);
+ canvas.drawRect(0, 0, width, height, paint);
+ } else {
+ // Put a tiny red dot on the screen so a knowledgeable user can check whether
+ // it is enabled. The dot is actually a zero-width, zero-height rectangle,
+ // placed at the lower-right corner of the canvas, painted with a non-zero border
+ // width.
+ paint.setStrokeWidth(3);
+ canvas.drawRect(width, height, width, height, paint);
+ }
+ paint.setColor(savedColor);
+ paint.setStyle(savedStyle);
+ paint.setStrokeWidth(savedStrokeWidth);
+ }
+ }
+
+ private static final Object[] EVENTKEYS_NULLVALUES = {};
+
+ /**
+ * Buffer a research log event, flagging it as privacy-sensitive.
+ *
+ * This event contains potentially private information. If the word that this event is a part
+ * of is determined to be privacy-sensitive, then this event should not be included in the
+ * output log. The system waits to output until the containing word is known.
+ *
+ * @param keys an array containing a descriptive name for the event, followed by the keys
+ * @param values an array of values, either a String or Number. length should be one
+ * less than the keys array
+ */
+ private synchronized void enqueuePotentiallyPrivateEvent(final String[] keys,
+ final Object[] values) {
+ assert values.length + 1 == keys.length;
+ if (isAllowedToLog()) {
+ mCurrentLogUnit.addLogStatement(keys, values, true /* isPotentiallyPrivate */);
+ }
+ }
+
+ private void setCurrentLogUnitContainsDigitFlag() {
+ mCurrentLogUnit.setContainsDigit();
+ }
+
+ /**
+ * Buffer a research log event, flaggint it as not privacy-sensitive.
+ *
+ * This event contains no potentially private information. Even if the word that this event
+ * is privacy-sensitive, this event can still safely be sent to the output log. The system
+ * waits until the containing word is known so that this event can be written in the proper
+ * temporal order with other events that may be privacy sensitive.
+ *
+ * @param keys an array containing a descriptive name for the event, followed by the keys
+ * @param values an array of values, either a String or Number. length should be one
+ * less than the keys array
+ */
+ private synchronized void enqueueEvent(final String[] keys, final Object[] values) {
+ assert values.length + 1 == keys.length;
+ if (isAllowedToLog()) {
+ mCurrentLogUnit.addLogStatement(keys, values, false /* isPotentiallyPrivate */);
+ }
+ }
+
+ /* package for test */ void commitCurrentLogUnit() {
+ if (DEBUG) {
+ Log.d(TAG, "commitCurrentLogUnit");
+ }
+ if (!mCurrentLogUnit.isEmpty()) {
+ if (mMainLogBuffer != null) {
+ mMainLogBuffer.shiftIn(mCurrentLogUnit);
+ if (mMainLogBuffer.isSafeToLog() && mMainResearchLog != null) {
+ publishLogBuffer(mMainLogBuffer, mMainResearchLog,
+ true /* isIncludingPrivateData */);
+ mMainLogBuffer.resetWordCounter();
+ }
+ }
+ if (mFeedbackLogBuffer != null) {
+ mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
+ }
+ mCurrentLogUnit = new LogUnit();
+ Log.d(TAG, "commitCurrentLogUnit");
+ }
+ }
+
+ /* package for test */ void publishLogBuffer(final LogBuffer logBuffer,
+ final ResearchLog researchLog, final boolean isIncludingPrivateData) {
+ LogUnit logUnit;
+ while ((logUnit = logBuffer.shiftOut()) != null) {
+ researchLog.publish(logUnit, isIncludingPrivateData);
+ }
+ }
+
+ private boolean hasOnlyLetters(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 (!Character.isLetter(codePoint)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void onWordComplete(final String word) {
+ Log.d(TAG, "onWordComplete: " + word);
+ if (word != null && word.length() > 0 && hasOnlyLetters(word)) {
+ mCurrentLogUnit.setWord(word);
+ mStatistics.recordWordEntered();
+ }
+ commitCurrentLogUnit();
+ }
+
+ private static int scrubDigitFromCodePoint(int codePoint) {
+ return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
+ }
+
+ /* package for test */ static String scrubDigitsFromString(String s) {
+ StringBuilder sb = null;
+ final int length = s.length();
+ for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) {
+ final int codePoint = Character.codePointAt(s, i);
+ if (Character.isDigit(codePoint)) {
+ if (sb == null) {
+ sb = new StringBuilder(length);
+ sb.append(s.substring(0, i));
+ }
+ sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT);
+ } else {
+ if (sb != null) {
+ sb.appendCodePoint(codePoint);
+ }
+ }
+ }
+ if (sb == null) {
+ return s;
+ } else {
+ return sb.toString();
+ }
+ }
+
+ private static String getUUID(final SharedPreferences prefs) {
+ String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null);
+ if (null == uuidString) {
+ UUID uuid = UUID.randomUUID();
+ uuidString = uuid.toString();
+ Editor editor = prefs.edit();
+ editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString);
+ editor.apply();
+ }
+ return uuidString;
+ }
+
+ private String scrubWord(String word) {
+ if (mDictionary == null) {
+ return WORD_REPLACEMENT_STRING;
+ }
+ if (mDictionary.isValidWord(word)) {
+ return word;
+ }
+ return WORD_REPLACEMENT_STRING;
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = {
+ "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions",
+ "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion"
+ };
+ public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
+ final SharedPreferences prefs) {
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.start();
+ if (editorInfo != null) {
+ final Context context = researchLogger.mInputMethodService;
+ try {
+ final PackageInfo packageInfo;
+ packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),
+ 0);
+ final Integer versionCode = packageInfo.versionCode;
+ final String versionName = packageInfo.versionName;
+ final Object[] values = {
+ researchLogger.mUUIDString, editorInfo.packageName,
+ Integer.toHexString(editorInfo.inputType),
+ Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
+ Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
+ OUTPUT_FORMAT_VERSION
+ };
+ researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values);
+ } catch (NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void latinIME_onFinishInputInternal() {
+ stop();
+ }
+
+ private static final String[] EVENTKEYS_USER_FEEDBACK = {
+ "UserFeedback", "FeedbackContents"
+ };
+
+ private static final String[] EVENTKEYS_PREFS_CHANGED = {
+ "PrefsChanged", "prefs"
+ };
+ public static void prefsChanged(final SharedPreferences prefs) {
+ final ResearchLogger researchLogger = getInstance();
+ final Object[] values = {
+ prefs
+ };
+ researchLogger.enqueueEvent(EVENTKEYS_PREFS_CHANGED, values);
+ }
+
+ // Regular logging methods
+
+ private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT = {
+ "MainKeyboardViewProcessMotionEvent", "action", "eventTime", "id", "x", "y", "size",
+ "pressure"
+ };
+ 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;
+ switch (action) {
+ case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break;
+ case MotionEvent.ACTION_UP: actionString = "UP"; break;
+ case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break;
+ case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break;
+ case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break;
+ case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break;
+ case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break;
+ default: actionString = "ACTION_" + action; break;
+ }
+ final float size = me.getSize(index);
+ final float pressure = me.getPressure(index);
+ final Object[] values = {
+ actionString, eventTime, id, x, y, size, pressure
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_MAINKEYBOARDVIEW_PROCESSMOTIONEVENT, values);
+ }
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_ONCODEINPUT = {
+ "LatinIMEOnCodeInput", "code", "x", "y"
+ };
+ public static void latinIME_onCodeInput(final int code, final int x, final int y) {
+ final long time = SystemClock.uptimeMillis();
+ final ResearchLogger researchLogger = getInstance();
+ final Object[] values = {
+ Keyboard.printableCode(scrubDigitFromCodePoint(code)), x, y
+ };
+ researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONCODEINPUT, values);
+ if (Character.isDigit(code)) {
+ researchLogger.setCurrentLogUnitContainsDigitFlag();
+ }
+ researchLogger.mStatistics.recordChar(code, time);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS = {
+ "LatinIMEOnDisplayCompletions", "applicationSpecifiedCompletions"
+ };
+ public static void latinIME_onDisplayCompletions(
+ final CompletionInfo[] applicationSpecifiedCompletions) {
+ final Object[] values = {
+ applicationSpecifiedCompletions
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONDISPLAYCOMPLETIONS,
+ values);
+ }
+
+ public static boolean getAndClearLatinIMEExpectingUpdateSelection() {
+ boolean returnValue = sLatinIMEExpectingUpdateSelection;
+ sLatinIMEExpectingUpdateSelection = false;
+ return returnValue;
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_ONWINDOWHIDDEN = {
+ "LatinIMEOnWindowHidden", "isTextTruncated", "text"
+ };
+ public static void latinIME_onWindowHidden(final int savedSelectionStart,
+ final int savedSelectionEnd, final InputConnection ic) {
+ if (ic != null) {
+ // Capture the TextView contents. This will trigger onUpdateSelection(), so we
+ // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called,
+ // it can tell that it was generated by the logging code, and not by the user, and
+ // therefore keep user-visible state as is.
+ ic.beginBatchEdit();
+ ic.performContextMenuAction(android.R.id.selectAll);
+ CharSequence charSequence = ic.getSelectedText(0);
+ ic.setSelection(savedSelectionStart, savedSelectionEnd);
+ ic.endBatchEdit();
+ sLatinIMEExpectingUpdateSelection = true;
+ final Object[] values = new Object[2];
+ if (OUTPUT_ENTIRE_BUFFER) {
+ if (TextUtils.isEmpty(charSequence)) {
+ values[0] = false;
+ values[1] = "";
+ } else {
+ if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
+ int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
+ // do not cut in the middle of a supplementary character
+ final char c = charSequence.charAt(length - 1);
+ if (Character.isHighSurrogate(c)) {
+ length--;
+ }
+ final CharSequence truncatedCharSequence = charSequence.subSequence(0,
+ length);
+ values[0] = true;
+ values[1] = truncatedCharSequence.toString();
+ } else {
+ values[0] = false;
+ values[1] = charSequence.toString();
+ }
+ }
+ } else {
+ values[0] = true;
+ values[1] = "";
+ }
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONWINDOWHIDDEN, values);
+ researchLogger.commitCurrentLogUnit();
+ getInstance().stop();
+ }
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_ONUPDATESELECTION = {
+ "LatinIMEOnUpdateSelection", "lastSelectionStart", "lastSelectionEnd", "oldSelStart",
+ "oldSelEnd", "newSelStart", "newSelEnd", "composingSpanStart", "composingSpanEnd",
+ "expectingUpdateSelection", "expectingUpdateSelectionFromLogger", "context"
+ };
+ public static void latinIME_onUpdateSelection(final int lastSelectionStart,
+ final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
+ final int newSelStart, final int newSelEnd, final int composingSpanStart,
+ final int composingSpanEnd, final boolean expectingUpdateSelection,
+ final boolean expectingUpdateSelectionFromLogger,
+ final RichInputConnection connection) {
+ String word = "";
+ if (connection != null) {
+ Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
+ if (range != null) {
+ word = range.mWord;
+ }
+ }
+ final ResearchLogger researchLogger = getInstance();
+ final String scrubbedWord = researchLogger.scrubWord(word);
+ final Object[] values = {
+ lastSelectionStart, lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart,
+ newSelEnd, composingSpanStart, composingSpanEnd, expectingUpdateSelection,
+ expectingUpdateSelectionFromLogger, scrubbedWord
+ };
+ researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_ONUPDATESELECTION, values);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY = {
+ "LatinIMEPickSuggestionManually", "replacedWord", "index", "suggestion", "x", "y"
+ };
+ public static void latinIME_pickSuggestionManually(final String replacedWord,
+ final int index, CharSequence suggestion) {
+ final Object[] values = {
+ scrubDigitsFromString(replacedWord), index,
+ (suggestion == null ? null : scrubDigitsFromString(suggestion.toString())),
+ Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE
+ };
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_PICKSUGGESTIONMANUALLY,
+ values);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION = {
+ "LatinIMEPunctuationSuggestion", "index", "suggestion", "x", "y"
+ };
+ public static void latinIME_punctuationSuggestion(final int index,
+ final CharSequence suggestion) {
+ final Object[] values = {
+ index, suggestion,
+ Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE
+ };
+ getInstance().enqueueEvent(EVENTKEYS_LATINIME_PUNCTUATIONSUGGESTION, values);
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_SENDKEYCODEPOINT = {
+ "LatinIMESendKeyCodePoint", "code"
+ };
+ public static void latinIME_sendKeyCodePoint(final int code) {
+ final Object[] values = {
+ Keyboard.printableCode(scrubDigitFromCodePoint(code))
+ };
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_SENDKEYCODEPOINT, values);
+ if (Character.isDigit(code)) {
+ researchLogger.setCurrentLogUnitContainsDigitFlag();
+ }
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE = {
+ "LatinIMESwapSwapperAndSpace"
+ };
+ public static void latinIME_swapSwapperAndSpace() {
+ getInstance().enqueueEvent(EVENTKEYS_LATINIME_SWAPSWAPPERANDSPACE, EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS = {
+ "MainKeyboardViewOnLongPress"
+ };
+ public static void mainKeyboardView_onLongPress() {
+ getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_ONLONGPRESS, EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD = {
+ "MainKeyboardViewSetKeyboard", "elementId", "locale", "orientation", "width",
+ "modeName", "action", "navigateNext", "navigatePrevious", "clobberSettingsKey",
+ "passwordInput", "shortcutKeyEnabled", "hasShortcutKey", "languageSwitchKeyEnabled",
+ "isMultiLine", "tw", "th", "keys"
+ };
+ public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) {
+ if (keyboard != null) {
+ final KeyboardId kid = keyboard.mId;
+ final boolean isPasswordView = kid.passwordInput();
+ getInstance().setIsPasswordView(isPasswordView);
+ final Object[] values = {
+ KeyboardId.elementIdToName(kid.mElementId),
+ kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
+ kid.mOrientation,
+ 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
+ };
+ getInstance().setIsPasswordView(isPasswordView);
+ getInstance().enqueueEvent(EVENTKEYS_MAINKEYBOARDVIEW_SETKEYBOARD, values);
+ }
+ }
+
+ private static final String[] EVENTKEYS_LATINIME_REVERTCOMMIT = {
+ "LatinIMERevertCommit", "originallyTypedWord"
+ };
+ public static void latinIME_revertCommit(final String originallyTypedWord) {
+ final Object[] values = {
+ originallyTypedWord
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_REVERTCOMMIT, values);
+ }
+
+ private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT = {
+ "PointerTrackerCallListenerOnCancelInput"
+ };
+ public static void pointerTracker_callListenerOnCancelInput() {
+ getInstance().enqueueEvent(EVENTKEYS_POINTERTRACKER_CALLLISTENERONCANCELINPUT,
+ EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT = {
+ "PointerTrackerCallListenerOnCodeInput", "code", "outputText", "x", "y",
+ "ignoreModifierKey", "altersCode", "isEnabled"
+ };
+ public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
+ final int y, final boolean ignoreModifierKey, final boolean altersCode,
+ final int code) {
+ if (key != null) {
+ String outputText = key.getOutputText();
+ final Object[] values = {
+ Keyboard.printableCode(scrubDigitFromCodePoint(code)), outputText == null ? null
+ : scrubDigitsFromString(outputText.toString()),
+ x, y, ignoreModifierKey, altersCode, key.isEnabled()
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_POINTERTRACKER_CALLLISTENERONCODEINPUT, values);
+ }
+ }
+
+ private static final String[] EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE = {
+ "PointerTrackerCallListenerOnRelease", "code", "withSliding", "ignoreModifierKey",
+ "isEnabled"
+ };
+ public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
+ final boolean withSliding, final boolean ignoreModifierKey) {
+ if (key != null) {
+ final Object[] values = {
+ Keyboard.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
+ ignoreModifierKey, key.isEnabled()
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_POINTERTRACKER_CALLLISTENERONRELEASE, values);
+ }
+ }
+
+ private static final String[] EVENTKEYS_POINTERTRACKER_ONDOWNEVENT = {
+ "PointerTrackerOnDownEvent", "deltaT", "distanceSquared"
+ };
+ public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
+ final Object[] values = {
+ deltaT, distanceSquared
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONDOWNEVENT, values);
+ }
+
+ private static final String[] EVENTKEYS_POINTERTRACKER_ONMOVEEVENT = {
+ "PointerTrackerOnMoveEvent", "x", "y", "lastX", "lastY"
+ };
+ public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
+ final int lastY) {
+ final Object[] values = {
+ x, y, lastX, lastY
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_POINTERTRACKER_ONMOVEEVENT, values);
+ }
+
+ private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION = {
+ "RichInputConnectionCommitCompletion", "completionInfo"
+ };
+ public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) {
+ final Object[] values = {
+ completionInfo
+ };
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_RICHINPUTCONNECTION_COMMITCOMPLETION, values);
+ }
+
+ // Disabled for privacy-protection reasons. Because this event comes after
+ // richInputConnection_commitText, which is the event used to separate LogUnits, the
+ // data in this event can be associated with the next LogUnit, revealing information
+ // about the current word even if it was supposed to be suppressed. The occurrance of
+ // autocorrection can be determined by examining the difference between the text strings in
+ // the last call to richInputConnection_setComposingText before
+ // richInputConnection_commitText, so it's not a data loss.
+ // TODO: Figure out how to log this event without loss of privacy.
+ /*
+ private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION = {
+ "RichInputConnectionCommitCorrection", "typedWord", "autoCorrection"
+ };
+ */
+ public static void richInputConnection_commitCorrection(CorrectionInfo correctionInfo) {
+ /*
+ final String typedWord = correctionInfo.getOldText().toString();
+ final String autoCorrection = correctionInfo.getNewText().toString();
+ final Object[] values = {
+ scrubDigitsFromString(typedWord), scrubDigitsFromString(autoCorrection)
+ };
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_RICHINPUTCONNECTION_COMMITCORRECTION, values);
+ */
+ }
+
+ private static final String[] EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT = {
+ "RichInputConnectionCommitText", "typedWord", "newCursorPosition"
+ };
+ public static void richInputConnection_commitText(final CharSequence typedWord,
+ final int newCursorPosition) {
+ final String scrubbedWord = scrubDigitsFromString(typedWord.toString());
+ final Object[] values = {
+ scrubbedWord, newCursorPosition
+ };
+ final ResearchLogger researchLogger = getInstance();
+ researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_COMMITTEXT,
+ values);
+ researchLogger.onWordComplete(scrubbedWord);
+ }
+
+ private static final String[] EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT = {
+ "RichInputConnectionDeleteSurroundingText", "beforeLength", "afterLength"
+ };
+ public static void richInputConnection_deleteSurroundingText(final int beforeLength,
+ final int afterLength) {
+ final Object[] values = {
+ beforeLength, afterLength
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT, values);
+ }
+
+ private static final String[] EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT = {
+ "RichInputConnectionFinishComposingText"
+ };
+ public static void richInputConnection_finishComposingText() {
+ getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT,
+ EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION = {
+ "RichInputConnectionPerformEditorAction", "imeActionNext"
+ };
+ public static void richInputConnection_performEditorAction(final int imeActionNext) {
+ final Object[] values = {
+ imeActionNext
+ };
+ getInstance().enqueueEvent(EVENTKEYS_RICHINPUTCONNECTION_PERFORMEDITORACTION, values);
+ }
+
+ private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT = {
+ "RichInputConnectionSendKeyEvent", "eventTime", "action", "code"
+ };
+ public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) {
+ final Object[] values = {
+ keyEvent.getEventTime(),
+ keyEvent.getAction(),
+ keyEvent.getKeyCode()
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SENDKEYEVENT,
+ values);
+ }
+
+ private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT = {
+ "RichInputConnectionSetComposingText", "text", "newCursorPosition"
+ };
+ public static void richInputConnection_setComposingText(final CharSequence text,
+ final int newCursorPosition) {
+ if (text == null) {
+ throw new RuntimeException("setComposingText is null");
+ }
+ final Object[] values = {
+ text, newCursorPosition
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETCOMPOSINGTEXT,
+ values);
+ }
+
+ private static final String[] EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION = {
+ "RichInputConnectionSetSelection", "from", "to"
+ };
+ public static void richInputConnection_setSelection(final int from, final int to) {
+ final Object[] values = {
+ from, to
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(EVENTKEYS_RICHINPUTCONNECTION_SETSELECTION,
+ values);
+ }
+
+ private static final String[] EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT = {
+ "SuddenJumpingTouchEventHandlerOnTouchEvent", "motionEvent"
+ };
+ public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
+ if (me != null) {
+ final Object[] values = {
+ me.toString()
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT, values);
+ }
+ }
+
+ private static final String[] EVENTKEYS_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS = {
+ "SuggestionStripViewSetSuggestions", "suggestedWords"
+ };
+ public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) {
+ if (suggestedWords != null) {
+ final Object[] values = {
+ suggestedWords
+ };
+ getInstance().enqueuePotentiallyPrivateEvent(
+ EVENTKEYS_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS, values);
+ }
+ }
+
+ private static final String[] EVENTKEYS_USER_TIMESTAMP = {
+ "UserTimestamp"
+ };
+ public void userTimestamp() {
+ getInstance().enqueueEvent(EVENTKEYS_USER_TIMESTAMP, EVENTKEYS_NULLVALUES);
+ }
+
+ private static final String[] EVENTKEYS_STATISTICS = {
+ "Statistics", "charCount", "letterCount", "numberCount", "spaceCount", "deleteOpsCount",
+ "wordCount", "isEmptyUponStarting", "isEmptinessStateKnown", "averageTimeBetweenKeys",
+ "averageTimeBeforeDelete", "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete"
+ };
+ private static void logStatistics() {
+ final ResearchLogger researchLogger = getInstance();
+ final Statistics statistics = researchLogger.mStatistics;
+ final Object[] values = {
+ statistics.mCharCount, statistics.mLetterCount, statistics.mNumberCount,
+ statistics.mSpaceCount, statistics.mDeleteKeyCount,
+ statistics.mWordCount, statistics.mIsEmptyUponStarting,
+ statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(),
+ statistics.mBeforeDeleteKeyCounter.getAverageTime(),
+ statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(),
+ statistics.mAfterDeleteKeyCounter.getAverageTime()
+ };
+ researchLogger.enqueueEvent(EVENTKEYS_STATISTICS, values);
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java
new file mode 100644
index 000000000..eab465aa2
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/Statistics.java
@@ -0,0 +1,146 @@
+/*
+ * 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.research;
+
+import com.android.inputmethod.keyboard.Keyboard;
+
+public class Statistics {
+ // Number of characters entered during a typing session
+ int mCharCount;
+ // Number of letter characters entered during a typing session
+ int mLetterCount;
+ // Number of number characters entered
+ int mNumberCount;
+ // Number of space characters entered
+ int mSpaceCount;
+ // Number of delete operations entered (taps on the backspace key)
+ int mDeleteKeyCount;
+ // Number of words entered during a session.
+ int mWordCount;
+ // Whether the text field was empty upon editing
+ boolean mIsEmptyUponStarting;
+ boolean mIsEmptinessStateKnown;
+
+ // Timers to count average time to enter a key, first press a delete key,
+ // between delete keys, and then to return typing after a delete key.
+ final AverageTimeCounter mKeyCounter = new AverageTimeCounter();
+ final AverageTimeCounter mBeforeDeleteKeyCounter = new AverageTimeCounter();
+ final AverageTimeCounter mDuringRepeatedDeleteKeysCounter = new AverageTimeCounter();
+ final AverageTimeCounter mAfterDeleteKeyCounter = new AverageTimeCounter();
+
+ static class AverageTimeCounter {
+ int mCount;
+ int mTotalTime;
+
+ public void reset() {
+ mCount = 0;
+ mTotalTime = 0;
+ }
+
+ public void add(long deltaTime) {
+ mCount++;
+ mTotalTime += deltaTime;
+ }
+
+ public int getAverageTime() {
+ if (mCount == 0) {
+ return 0;
+ }
+ return mTotalTime / mCount;
+ }
+ }
+
+ // 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
+
+ // The last time that a tap was performed
+ private long mLastTapTime;
+ // The type of the last keypress (delete key or not)
+ boolean mIsLastKeyDeleteKey;
+
+ private static final Statistics sInstance = new Statistics();
+
+ public static Statistics getInstance() {
+ return sInstance;
+ }
+
+ private Statistics() {
+ reset();
+ }
+
+ public void reset() {
+ mCharCount = 0;
+ mLetterCount = 0;
+ mNumberCount = 0;
+ mSpaceCount = 0;
+ mDeleteKeyCount = 0;
+ mWordCount = 0;
+ mIsEmptyUponStarting = true;
+ mIsEmptinessStateKnown = false;
+ mKeyCounter.reset();
+ mBeforeDeleteKeyCounter.reset();
+ mDuringRepeatedDeleteKeysCounter.reset();
+ mAfterDeleteKeyCounter.reset();
+
+ mLastTapTime = 0;
+ mIsLastKeyDeleteKey = false;
+ }
+
+ public void recordChar(int codePoint, long time) {
+ final long delta = time - mLastTapTime;
+ if (codePoint == Keyboard.CODE_DELETE) {
+ mDeleteKeyCount++;
+ if (delta < MIN_DELETION_INTERMISSION) {
+ if (mIsLastKeyDeleteKey) {
+ mDuringRepeatedDeleteKeysCounter.add(delta);
+ } else {
+ mBeforeDeleteKeyCounter.add(delta);
+ }
+ }
+ mIsLastKeyDeleteKey = true;
+ } else {
+ mCharCount++;
+ if (Character.isDigit(codePoint)) {
+ mNumberCount++;
+ }
+ if (Character.isLetter(codePoint)) {
+ mLetterCount++;
+ }
+ if (Character.isSpaceChar(codePoint)) {
+ mSpaceCount++;
+ }
+ if (mIsLastKeyDeleteKey && delta < MIN_DELETION_INTERMISSION) {
+ mAfterDeleteKeyCounter.add(delta);
+ } else if (!mIsLastKeyDeleteKey && delta < MIN_TYPING_INTERMISSION) {
+ mKeyCounter.add(delta);
+ }
+ mIsLastKeyDeleteKey = false;
+ }
+ mLastTapTime = time;
+ }
+
+ public void recordWordEntered() {
+ mWordCount++;
+ }
+
+ public void setIsEmptyUponStarting(final boolean isEmpty) {
+ mIsEmptyUponStarting = isEmpty;
+ mIsEmptinessStateKnown = true;
+ }
+}
diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java
new file mode 100644
index 000000000..7a5749096
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/UploaderService.java
@@ -0,0 +1,191 @@
+/*
+ * 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.research;
+
+import android.Manifest;
+import android.app.AlarmManager;
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.inputmethod.latin.R;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public final class UploaderService extends IntentService {
+ private static final String TAG = UploaderService.class.getSimpleName();
+ public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR;
+ private static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
+ + ".extra.UPLOAD_UNCONDITIONALLY";
+ private static final int BUF_SIZE = 1024 * 8;
+ protected static final int TIMEOUT_IN_MS = 1000 * 4;
+
+ private boolean mCanUpload;
+ private File mFilesDir;
+ private URL mUrl;
+
+ public UploaderService() {
+ super("Research Uploader Service");
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mCanUpload = false;
+ mFilesDir = null;
+ mUrl = null;
+
+ final PackageManager packageManager = getPackageManager();
+ final boolean hasPermission = packageManager.checkPermission(Manifest.permission.INTERNET,
+ getPackageName()) == PackageManager.PERMISSION_GRANTED;
+ if (!hasPermission) {
+ return;
+ }
+
+ try {
+ final String urlString = getString(R.string.research_logger_upload_url);
+ if (urlString == null || urlString.equals("")) {
+ return;
+ }
+ mFilesDir = getFilesDir();
+ mUrl = new URL(urlString);
+ mCanUpload = true;
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (!mCanUpload) {
+ return;
+ }
+ boolean isUploadingUnconditionally = false;
+ Bundle bundle = intent.getExtras();
+ if (bundle != null && bundle.containsKey(EXTRA_UPLOAD_UNCONDITIONALLY)) {
+ isUploadingUnconditionally = bundle.getBoolean(EXTRA_UPLOAD_UNCONDITIONALLY);
+ }
+ doUpload(isUploadingUnconditionally);
+ }
+
+ private boolean isExternallyPowered() {
+ final Intent intent = registerReceiver(null, new IntentFilter(
+ Intent.ACTION_BATTERY_CHANGED));
+ final int pluggedState = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
+ return pluggedState == BatteryManager.BATTERY_PLUGGED_AC
+ || pluggedState == BatteryManager.BATTERY_PLUGGED_USB;
+ }
+
+ private boolean hasWifiConnection() {
+ final ConnectivityManager manager =
+ (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+ final NetworkInfo wifiInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+ return wifiInfo.isConnected();
+ }
+
+ private void doUpload(final boolean isUploadingUnconditionally) {
+ if (!isUploadingUnconditionally && (!isExternallyPowered() || !hasWifiConnection())) {
+ return;
+ }
+ if (mFilesDir == null) {
+ return;
+ }
+ final File[] files = mFilesDir.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ return pathname.getName().startsWith(ResearchLogger.FILENAME_PREFIX)
+ && !pathname.canWrite();
+ }
+ });
+ boolean success = true;
+ if (files.length == 0) {
+ success = false;
+ }
+ for (final File file : files) {
+ if (!uploadFile(file)) {
+ success = false;
+ }
+ }
+ }
+
+ private boolean uploadFile(File file) {
+ Log.d(TAG, "attempting upload of " + file.getAbsolutePath());
+ boolean success = false;
+ final int contentLength = (int) file.length();
+ HttpURLConnection connection = null;
+ InputStream fileInputStream = null;
+ try {
+ fileInputStream = new FileInputStream(file);
+ connection = (HttpURLConnection) mUrl.openConnection();
+ connection.setRequestMethod("PUT");
+ connection.setDoOutput(true);
+ connection.setFixedLengthStreamingMode(contentLength);
+ final OutputStream os = connection.getOutputStream();
+ final byte[] buf = new byte[BUF_SIZE];
+ int numBytesRead;
+ while ((numBytesRead = fileInputStream.read(buf)) != -1) {
+ os.write(buf, 0, numBytesRead);
+ }
+ if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ Log.d(TAG, "upload failed: " + connection.getResponseCode());
+ InputStream netInputStream = connection.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(netInputStream));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ Log.d(TAG, "| " + reader.readLine());
+ }
+ reader.close();
+ return success;
+ }
+ file.delete();
+ success = true;
+ Log.d(TAG, "upload successful");
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (fileInputStream != null) {
+ try {
+ fileInputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ return success;
+ }
+}