diff options
Diffstat (limited to 'java/src')
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> - * >!-- xml/keyboard.xml --< - * >Keyboard keyboard_attributes*< - * >!-- Keyboard Content --< - * >Row row_attributes*< - * >!-- Row Content --< - * >Key key_attributes* /< - * >Spacer horizontalGap="32.0dp" /< - * >include keyboardLayout="@xml/other_keys"< - * ... - * >/Row< - * >include keyboardLayout="@xml/other_rows"< - * ... - * >/Keyboard< - * </pre> - * The XML file which is included in other file must have >merge< as root element, - * such as: - * <pre> - * >!-- xml/other_keys.xml --< - * >merge< - * >Key key_attributes* /< - * ... - * >/merge< - * </pre> - * and - * <pre> - * >!-- xml/other_rows.xml --< - * >merge< - * >Row row_attributes*< - * >Key key_attributes* /< - * >/Row< - * ... - * >/merge< - * </pre> - * You can also use switch-case-default tags to select Rows and Keys. - * <pre> - * >switch< - * >case case_attribute*< - * >!-- Any valid tags at switch position --< - * >/case< - * ... - * >default< - * >!-- Any valid tags at switch position --< - * >/default< - * >/switch< - * </pre> - * You can declare Key style and specify styles within Key tags. - * <pre> - * >switch< - * >case mode="email"< - * >key-style styleName="f1-key" parentStyle="modifier-key" - * keyLabel=".com" - * /< - * >/case< - * >case mode="url"< - * >key-style styleName="f1-key" parentStyle="modifier-key" - * keyLabel="http://" - * /< - * >/case< - * >/switch< - * ... - * >Key keyStyle="shift-key" ... /< - * </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> + * <!-- xml/keyboard.xml --> + * <Keyboard keyboard_attributes*> + * <!-- Keyboard Content --> + * <Row row_attributes*> + * <!-- Row Content --> + * <Key key_attributes* /> + * <Spacer horizontalGap="32.0dp" /> + * <include keyboardLayout="@xml/other_keys"> + * ... + * </Row> + * <include keyboardLayout="@xml/other_rows"> + * ... + * </Keyboard> + * </pre> + * The XML file which is included in other file must have <merge> as root element, + * such as: + * <pre> + * <!-- xml/other_keys.xml --> + * <merge> + * <Key key_attributes* /> + * ... + * </merge> + * </pre> + * and + * <pre> + * <!-- xml/other_rows.xml --> + * <merge> + * <Row row_attributes*> + * <Key key_attributes* /> + * </Row> + * ... + * </merge> + * </pre> + * You can also use switch-case-default tags to select Rows and Keys. + * <pre> + * <switch> + * <case case_attribute*> + * <!-- Any valid tags at switch position --> + * </case> + * ... + * <default> + * <!-- Any valid tags at switch position --> + * </default> + * </switch> + * </pre> + * You can declare Key style and specify styles within Key tags. + * <pre> + * <switch> + * <case mode="email"> + * <key-style styleName="f1-key" parentStyle="modifier-key" + * keyLabel=".com" + * /> + * </case> + * <case mode="url"> + * <key-style styleName="f1-key" parentStyle="modifier-key" + * keyLabel="http://" + * /> + * </case> + * </switch> + * ... + * <Key keyStyle="shift-key" ... /> + * </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,“,”,„,‟,«,»</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,“,”,„,‟,«,»,‘,’,‚,‛</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">“,”,„,‟,«|»,»|«</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,“,”,„,‟,«|»,»|«;,‘,’,‚,‛</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">“,”,„,‟,«|»,»|«</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,“,”,„,‟,«|»,»|«;,‘,’,‚,‛</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">“,”,„,‟,«|»,»|«</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,“,”,„,‟,«|»,»|«;,‘,’,‚,‛</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,„,“,”,‟,«,»</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,“,”,„,‟,«,»,‘,’,‚,‛</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">š,ß,ś</string> + // U+010D: "č" LATIN SMALL LETTER C WITH CARON + // U+00E7: "ç" LATIN SMALL LETTER C WITH CEDILLA + // U+0107: "ć" LATIN SMALL LETTER C WITH ACUTE + // <string name="more_keys_for_c">č,ç,ć</string> + // U+010F: "ď" LATIN SMALL LETTER D WITH CARON + // <string name="more_keys_for_d">ď</string> + // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON + // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE + // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE + // <string name="more_keys_for_z">ž,ź,ż</string> + // END: More keys definitions for Serbian (Latin) + // BEGIN: More keys definitions for Serbian (Cyrillic) // U+0437: "з" CYRILLIC SMALL LETTER ZE - /* 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,„,“,”,‟,«,»</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,“,”,„,‟,«,»,‘,’,‚,‛</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; + } +} |